1 #include "sqleditor.h"
2 #include "log.h"
3 #include "uiconfig.h"
4 #include "uiutils.h"
5 #include "services/config.h"
6 #include "iconmanager.h"
7 #include "completer/completerwindow.h"
8 #include "completionhelper.h"
9 #include "common/utils_sql.h"
10 #include "parser/lexer.h"
11 #include "parser/parser.h"
12 #include "parser/parsererror.h"
13 #include "common/unused.h"
14 #include "services/notifymanager.h"
15 #include "dialogs/searchtextdialog.h"
16 #include "dbobjectdialogs.h"
17 #include "searchtextlocator.h"
18 #include "services/codeformatter.h"
19 #include "sqlitestudio.h"
20 #include "style.h"
21 #include "dbtree/dbtreeitem.h"
22 #include "dbtree/dbtree.h"
23 #include "dbtree/dbtreemodel.h"
24 #include "common/lazytrigger.h"
25 #include <QAction>
26 #include <QMenu>
27 #include <QTimer>
28 #include <QDebug>
29 #include <QKeyEvent>
30 #include <QPainter>
31 #include <QTextBlock>
32 #include <QScrollBar>
33 #include <QFileDialog>
34 #include <QtConcurrent/QtConcurrent>
35 #include <QStyle>
36 
CFG_KEYS_DEFINE(SqlEditor)37 CFG_KEYS_DEFINE(SqlEditor)
38 
39 SqlEditor::SqlEditor(QWidget *parent) :
40     QPlainTextEdit(parent)
41 {
42     init();
43 }
44 
~SqlEditor()45 SqlEditor::~SqlEditor()
46 {
47     if (objectsInNamedDbFuture.isRunning())
48         objectsInNamedDbFuture.waitForFinished();
49 
50     if (queryParser)
51     {
52         delete queryParser;
53         queryParser = nullptr;
54     }
55 }
56 
init()57 void SqlEditor::init()
58 {
59     highlighter = new SqliteSyntaxHighlighter(document());
60     setFont(CFG_UI.Fonts.SqlEditor.get());
61     initActions();
62     setupMenu();
63 
64     textLocator = new SearchTextLocator(document(), this);
65     connect(textLocator, SIGNAL(found(int,int)), this, SLOT(found(int,int)));
66     connect(textLocator, SIGNAL(reachedEnd()), this, SLOT(reachedEnd()));
67 
68     lineNumberArea = new LineNumberArea(this);
69 
70     connect(this, SIGNAL(blockCountChanged(int)), this, SLOT(updateLineNumberAreaWidth()));
71     connect(this, SIGNAL(updateRequest(QRect,int)), this, SLOT(updateLineNumberArea(QRect,int)));
72     connect(this, SIGNAL(textChanged()), this, SLOT(checkContentSize()));
73     connect(this, SIGNAL(cursorPositionChanged()), this, SLOT(cursorMoved()));
74 
75     updateLineNumberAreaWidth();
76     highlightCurrentCursorContext();
77 
78     completer = new CompleterWindow(this);
79     connect(completer, SIGNAL(accepted()), this, SLOT(completeSelected()));
80     connect(completer, SIGNAL(textTyped(QString)), this, SLOT(completerTypedText(QString)));
81     connect(completer, SIGNAL(backspacePressed()), this, SLOT(completerBackspacePressed()));
82     connect(completer, SIGNAL(leftPressed()), this, SLOT(completerLeftPressed()));
83     connect(completer, SIGNAL(rightPressed()), this, SLOT(completerRightPressed()));
84 
85     autoCompleteTrigger = new LazyTrigger(autoCompleterDelay,
86                                           [this]() -> bool {return autoCompletion && !deletionKeyPressed;},
87                                           this);
88     connect(autoCompleteTrigger, SIGNAL(triggered()), this, SLOT(checkForAutoCompletion()));
89 
90     queryParserTrigger = new LazyTrigger(queryParserDelay, this);
91     connect(queryParserTrigger, SIGNAL(triggered()), this, SLOT(parseContents()));
92 
93     connect(this, SIGNAL(textChanged()), this, SLOT(scheduleQueryParser()));
94 
95     queryParser = new Parser();
96 
97     connect(this, &QWidget::customContextMenuRequested, this, &SqlEditor::customContextMenuRequested);
98     connect(CFG_UI.Fonts.SqlEditor, SIGNAL(changed(QVariant)), this, SLOT(changeFont(QVariant)));
99     connect(CFG, SIGNAL(massSaveCommitted()), this, SLOT(configModified()));
100 }
101 
removeErrorMarkers()102 void SqlEditor::removeErrorMarkers()
103 {
104     highlighter->clearErrors();
105 }
106 
haveErrors()107 bool SqlEditor::haveErrors()
108 {
109     return highlighter->haveErrors();
110 }
111 
isSyntaxChecked()112 bool SqlEditor::isSyntaxChecked()
113 {
114     return syntaxValidated;
115 }
116 
markErrorAt(int start,int end,bool limitedDamage)117 void SqlEditor::markErrorAt(int start, int end, bool limitedDamage)
118 {
119     highlighter->addError(start, end, limitedDamage);
120 }
121 
createActions()122 void SqlEditor::createActions()
123 {
124     createAction(CUT, ICONS.ACT_CUT, tr("Cut", "sql editor"), this, SLOT(cut()), this);
125     createAction(COPY, ICONS.ACT_COPY, tr("Copy", "sql editor"), this, SLOT(copy()), this);
126     createAction(PASTE, ICONS.ACT_PASTE, tr("Paste", "sql editor"), this, SLOT(paste()), this);
127     createAction(DELETE, ICONS.ACT_DELETE, tr("Delete", "sql editor"), this, SLOT(deleteSelected()), this);
128     createAction(SELECT_ALL, ICONS.ACT_SELECT_ALL, tr("Select all", "sql editor"), this, SLOT(selectAll()), this);
129     createAction(UNDO, ICONS.ACT_UNDO, tr("Undo", "sql editor"), this, SLOT(undo()), this);
130     createAction(REDO, ICONS.ACT_REDO, tr("Redo", "sql editor"), this, SLOT(redo()), this);
131     createAction(COMPLETE, ICONS.COMPLETE, tr("Complete", "sql editor"), this, SLOT(complete()), this);
132     createAction(FORMAT_SQL, ICONS.FORMAT_SQL, tr("Format SQL", "sql editor"), this, SLOT(formatSql()), this);
133     createAction(SAVE_SQL_FILE, ICONS.SAVE_SQL_FILE, tr("Save SQL to file", "sql editor"), this, SLOT(saveToFile()), this);
134     createAction(SAVE_AS_SQL_FILE, ICONS.SAVE_SQL_FILE, tr("Select file to save SQL", "sql editor"), this, SLOT(saveAsToFile()), this);
135     createAction(OPEN_SQL_FILE, ICONS.OPEN_SQL_FILE, tr("Load SQL from file", "sql editor"), this, SLOT(loadFromFile()), this);
136     createAction(DELETE_LINE, ICONS.ACT_DEL_LINE, tr("Delete line", "sql editor"), this, SLOT(deleteLine()), this);
137     createAction(MOVE_BLOCK_DOWN, tr("Move block down", "sql editor"), this, SLOT(moveBlockDown()), this);
138     createAction(MOVE_BLOCK_UP, tr("Move block up", "sql editor"), this, SLOT(moveBlockUp()), this);
139     createAction(COPY_BLOCK_DOWN, tr("Copy block down", "sql editor"), this, SLOT(copyBlockDown()), this);
140     createAction(COPY_BLOCK_UP, tr("Copy up down", "sql editor"), this, SLOT(copyBlockUp()), this);
141     createAction(FIND, ICONS.SEARCH, tr("Find", "sql editor"), this, SLOT(find()), this);
142     createAction(FIND_NEXT, tr("Find next", "sql editor"), this, SLOT(findNext()), this);
143     createAction(FIND_PREV, tr("Find previous", "sql editor"), this, SLOT(findPrevious()), this);
144     createAction(REPLACE, ICONS.SEARCH_AND_REPLACE, tr("Replace", "sql editor"), this, SLOT(replace()), this);
145     createAction(TOGGLE_COMMENT, tr("Toggle comment", "sql editor"), this, SLOT(toggleComment()), this);
146 
147     actionMap[CUT]->setEnabled(false);
148     actionMap[COPY]->setEnabled(false);
149     actionMap[UNDO]->setEnabled(false);
150     actionMap[REDO]->setEnabled(false);
151     actionMap[DELETE]->setEnabled(false);
152 
153     connect(this, &QPlainTextEdit::undoAvailable, this, &SqlEditor::updateUndoAction);
154     connect(this, &QPlainTextEdit::redoAvailable, this, &SqlEditor::updateRedoAction);
155     connect(this, &QPlainTextEdit::copyAvailable, this, &SqlEditor::updateCopyAction);
156 }
157 
setupDefShortcuts()158 void SqlEditor::setupDefShortcuts()
159 {
160     setShortcutContext({CUT, COPY, PASTE, DELETE, SELECT_ALL, UNDO, REDO, COMPLETE, FORMAT_SQL, SAVE_SQL_FILE, OPEN_SQL_FILE,
161                         DELETE_LINE}, Qt::WidgetWithChildrenShortcut);
162 
163     BIND_SHORTCUTS(SqlEditor, Action);
164 }
165 
setupMenu()166 void SqlEditor::setupMenu()
167 {
168     contextMenu = new QMenu(this);
169     contextMenu->addAction(actionMap[FORMAT_SQL]);
170     contextMenu->addSeparator();
171     contextMenu->addAction(actionMap[SAVE_SQL_FILE]);
172     contextMenu->addAction(actionMap[OPEN_SQL_FILE]);
173     contextMenu->addSeparator();
174     contextMenu->addAction(actionMap[UNDO]);
175     contextMenu->addAction(actionMap[REDO]);
176     contextMenu->addSeparator();
177     contextMenu->addAction(actionMap[FIND]);
178     contextMenu->addAction(actionMap[CUT]);
179     contextMenu->addAction(actionMap[COPY]);
180     contextMenu->addAction(actionMap[PASTE]);
181     contextMenu->addAction(actionMap[DELETE]);
182     contextMenu->addSeparator();
183     contextMenu->addAction(actionMap[SELECT_ALL]);
184 
185     validObjContextMenu = new QMenu(this);
186 }
187 
getDb() const188 Db* SqlEditor::getDb() const
189 {
190     return db;
191 }
192 
setDb(Db * value)193 void SqlEditor::setDb(Db* value)
194 {
195     db = value;
196     refreshValidObjects();
197     scheduleQueryParser(true);
198 }
199 
setAutoCompletion(bool enabled)200 void SqlEditor::setAutoCompletion(bool enabled)
201 {
202     autoCompletion = enabled;
203 }
204 
customContextMenuRequested(const QPoint & pos)205 void SqlEditor::customContextMenuRequested(const QPoint &pos)
206 {
207     if (objectLinksEnabled && handleValidObjectContextMenu(pos))
208         return;
209 
210     contextMenu->popup(mapToGlobal(pos));
211 }
212 
handleValidObjectContextMenu(const QPoint & pos)213 bool SqlEditor::handleValidObjectContextMenu(const QPoint& pos)
214 {
215     const DbObject* obj = getValidObjectForPosition(pos);
216     if (!obj)
217         return false;
218 
219     QString objName = stripObjName(toPlainText().mid(obj->from, (obj->to - obj->from + 1)));
220 
221     validObjContextMenu->clear();
222 
223     DbTreeItem* item = nullptr;
224     for (DbTreeItem::Type type : {DbTreeItem::Type::TABLE, DbTreeItem::Type::INDEX, DbTreeItem::Type::TRIGGER, DbTreeItem::Type::VIEW})
225     {
226         item = DBTREE->getModel()->findItem(type, objName);
227         if (item)
228             break;
229     }
230 
231     if (!item)
232         return false;
233 
234     DBTREE->setSelectedItem(item);
235     DBTREE->setupActionsForMenu(item, validObjContextMenu);
236     if (validObjContextMenu->actions().size() == 0)
237         return false;
238 
239     DBTREE->updateActionStates(item);
240     validObjContextMenu->popup(mapToGlobal(pos));
241     return true;
242 }
243 
saveToFile(const QString & fileName)244 void SqlEditor::saveToFile(const QString &fileName)
245 {
246     QFile file(fileName);
247     if (!file.open(QIODevice::WriteOnly | QIODevice::Text))
248     {
249         notifyError(tr("Could not open file '%1' for writing: %2").arg(fileName).arg(file.errorString()));
250         return;
251     }
252 
253     QTextStream stream(&file);
254     stream.setCodec("UTF-8");
255     stream << toPlainText();
256     stream.flush();
257     file.close();
258 
259     notifyInfo(tr("Saved SQL contents to file: %1").arg(fileName));
260 }
261 
toggleLineCommentForLine(const QTextBlock & block)262 void SqlEditor::toggleLineCommentForLine(const QTextBlock& block)
263 {
264     QTextCursor cur = textCursor();
265     QString line = block.text();
266     cur.setPosition(block.position());
267     if (line.startsWith("--"))
268     {
269         cur.deleteChar();
270         cur.deleteChar();
271     }
272     else
273         cur.insertText("--");
274 
275 }
276 
updateUndoAction(bool enabled)277 void SqlEditor::updateUndoAction(bool enabled)
278 {
279     actionMap[UNDO]->setEnabled(enabled);
280 }
281 
updateRedoAction(bool enabled)282 void SqlEditor::updateRedoAction(bool enabled)
283 {
284     actionMap[REDO]->setEnabled(enabled);
285 }
286 
updateCopyAction(bool enabled)287 void SqlEditor::updateCopyAction(bool enabled)
288 {
289     actionMap[COPY]->setEnabled(enabled);
290     actionMap[CUT]->setEnabled(enabled);
291     actionMap[DELETE]->setEnabled(enabled);
292 }
293 
deleteSelected()294 void SqlEditor::deleteSelected()
295 {
296     textCursor().removeSelectedText();
297 }
298 
homePressed(Qt::KeyboardModifiers modifiers)299 void SqlEditor::homePressed(Qt::KeyboardModifiers modifiers)
300 {
301     QTextCursor cursor = textCursor();
302 
303     bool shift = modifiers.testFlag(Qt::ShiftModifier);
304     QTextCursor::MoveMode mode = shift ? QTextCursor::KeepAnchor : QTextCursor::MoveAnchor;
305     if (modifiers.testFlag(Qt::ControlModifier))
306     {
307         cursor.setPosition(0, mode);
308         setTextCursor(cursor);
309         return;
310     }
311 
312     int curPos = cursor.positionInBlock();
313     QString line = cursor.block().text();
314     int firstPrintable = line.indexOf(QRegExp("\\S"));
315 
316     if (firstPrintable <= 0)
317     {
318         // If first printable character is the first character in line,
319         // or there's no printable characters at all, move to start of line.
320         cursor.movePosition(QTextCursor::StartOfLine, mode);
321     }
322     else if (curPos == 0 || curPos < firstPrintable)
323     {
324         // If cursor is at the line begining, or it's still before first printable character.
325         // Move to first printable character.
326         cursor.movePosition(QTextCursor::NextWord, mode);
327     }
328     else if (curPos == firstPrintable)
329     {
330         // If cursor is already at first printable character, now is the time to move it
331         // to start of the line.
332         cursor.movePosition(QTextCursor::StartOfLine, mode);
333     }
334     else
335     {
336         // Cursor is somewhere in the middle of printable text. Move it to the begining
337         // of line and then to first printable character.
338         cursor.movePosition(QTextCursor::StartOfLine, mode);
339         cursor.movePosition(QTextCursor::NextWord, mode);
340     }
341 
342     setTextCursor(cursor);
343 }
344 
tabPressed(bool shiftPressed)345 void SqlEditor::tabPressed(bool shiftPressed)
346 {
347     QTextCursor cursor = textCursor();
348     if (cursor.hasSelection())
349     {
350         indentSelected(shiftPressed);
351         return;
352     }
353 
354     // Get current line, its first printable character
355     int curPos = cursor.positionInBlock();
356     QString line = cursor.block().text();
357     int firstPrintable = line.indexOf(QRegExp("\\S"));
358 
359     // Handle shift+tab (unindent)
360     if (shiftPressed)
361     {
362         cursor.movePosition(QTextCursor::StartOfLine);
363 
364         if (firstPrintable > 0)
365             cursor.movePosition(QTextCursor::NextWord);
366 
367         setTextCursor(cursor);
368         backspacePressed();
369         return;
370     }
371 
372     // If we're past any printable character (and there was any), insert a tab
373     if (curPos > firstPrintable && firstPrintable >= 0)
374     {
375         insertPlainText("    ");
376         return;
377     }
378 
379     // If there is no previous block to refer to, insert a tab
380     QTextBlock previousBlock = document()->findBlockByNumber(cursor.blockNumber() - 1);
381     if (!previousBlock.isValid())
382     {
383         insertPlainText("    ");
384         return;
385     }
386 
387     // If previous block has first pritable character further than current cursor position, insert spaces to meet above position
388     int previousFirstPrintable = previousBlock.text().indexOf(QRegExp("\\S"));
389     if (curPos < previousFirstPrintable)
390     {
391         insertPlainText(QString(" ").repeated(previousFirstPrintable - curPos));
392         return;
393     }
394 
395     // At this point we know that previous block don't have first printable character further than the cursor. Insert tab.
396     insertPlainText("    ");
397 }
398 
backspacePressed()399 void SqlEditor::backspacePressed()
400 {
401     // If we have any selection, delete it and that's all.
402     QTextCursor cursor = textCursor();
403     if (cursor.hasSelection())
404     {
405         deleteSelected();
406         return;
407     }
408 
409     // No selection. Collect line, cursor position, first and last printable characters in line.
410     int curPos = cursor.positionInBlock();
411     QString line = cursor.block().text();
412     int firstPrintable = line.indexOf(QRegExp("\\S"));
413 
414     // If there is any printable character (which means that line length is greater than 0) and cursor is after first character,
415     // or when cursor is at the begining of line, delete previous character, always.
416     if ((firstPrintable > -1 && curPos > firstPrintable) || curPos == 0)
417     {
418         cursor.deletePreviousChar();
419         return;
420     }
421 
422     // Define number of spaces available for deletion.
423     int spaces = firstPrintable;
424     if (spaces < 0)
425         spaces = curPos;
426 
427     // Get previous block. If there was none, then delete up to 4 previous spaces.
428     QTextBlock previousBlock = document()->findBlockByNumber(cursor.blockNumber() - 1);
429     if (!previousBlock.isValid())
430     {
431         doBackspace(spaces < 4 ? spaces : 4);
432         return;
433     }
434 
435     // If first printable character in previous block is prior to the current cursor position (but not first in the line),
436     // delete as many spaces, as necessary to reach the same position, but never more than defined spaces number earlier.
437     int previousFirstPrintable = previousBlock.text().indexOf(QRegExp("\\S"));
438     if (curPos > previousFirstPrintable && previousFirstPrintable > 0)
439     {
440         int spacesToDelete = curPos - previousFirstPrintable;
441         doBackspace(spaces < spacesToDelete ? spaces : spacesToDelete);
442         return;
443     }
444 
445     // There is no character to back off to, so we simply delete up to 4 previous spaces.
446     doBackspace(spaces < 4 ? spaces : 4);
447 }
448 
complete()449 void SqlEditor::complete()
450 {
451     if (!db || !db->isValid())
452     {
453         notifyWarn(tr("Syntax completion can be used only when a valid database is set for the SQL editor."));
454         return;
455     }
456 
457     QString sql = toPlainText();
458     int curPos = textCursor().position();
459 
460     if (!virtualSqlExpression.isNull())
461     {
462         sql = virtualSqlExpression.arg(sql);
463         curPos += virtualSqlOffset;
464     }
465 
466     CompletionHelper completionHelper(sql, curPos, db);
467     completionHelper.setCreateTriggerTable(createTriggerTable);
468     CompletionHelper::Results result = completionHelper.getExpectedTokens();
469     if (result.filtered().size() == 0)
470         return;
471 
472     completer->setData(result);
473     completer->setDb(db);
474     if (completer->immediateResolution())
475         return;
476 
477     updateCompleterPosition();
478     completer->show();
479 }
480 
updateCompleterPosition()481 void SqlEditor::updateCompleterPosition()
482 {
483     QPoint pos = cursorRect().bottomRight();
484     pos += QPoint(1, fontMetrics().descent());
485     completer->move(mapToGlobal(pos));
486 }
487 
completeSelected()488 void SqlEditor::completeSelected()
489 {
490     deletePreviousChars(completer->getNumberOfCharsToRemove());
491 
492     ExpectedTokenPtr token = completer->getSelected();
493     QString value = token->value;
494     if (token->needsWrapping())
495         value = wrapObjIfNeeded(value);
496 
497     if (!token->prefix.isNull())
498     {
499         value.prepend(".");
500         value.prepend(wrapObjIfNeeded(token->prefix));
501     }
502 
503     insertPlainText(value);
504 }
505 
checkForAutoCompletion()506 void SqlEditor::checkForAutoCompletion()
507 {
508     if (!db || !autoCompletion || deletionKeyPressed || !richFeaturesEnabled)
509         return;
510 
511     Lexer lexer;
512     QString sql = toPlainText();
513     int curPos = textCursor().position();
514     TokenList tokens = lexer.tokenize(sql.left(curPos));
515 
516     if (tokens.size() > 0 && tokens.last()->type == Token::OPERATOR && tokens.last()->value == ".")
517         complete();
518 }
519 
deletePreviousChars(int length)520 void SqlEditor::deletePreviousChars(int length)
521 {
522     QTextCursor cursor = textCursor();
523     for (int i = 0; i < length; i++)
524         cursor.deletePreviousChar();
525 }
526 
refreshValidObjects()527 void SqlEditor::refreshValidObjects()
528 {
529     if (!db || !db->isValid())
530         return;
531 
532     objectsInNamedDbFuture = QtConcurrent::run([this]()
533     {
534         QMutexLocker lock(&objectsInNamedDbMutex);
535         objectsInNamedDb.clear();
536 
537         SchemaResolver resolver(db);
538         QSet<QString> databases = resolver.getDatabases();
539         databases << "main";
540         QStringList objects;
541         for (const QString& dbName : databases)
542         {
543             objects = resolver.getAllObjects(dbName);
544             objectsInNamedDb[dbName] << objects;
545         }
546     });
547 }
548 
setObjectLinks(bool enabled)549 void SqlEditor::setObjectLinks(bool enabled)
550 {
551     objectLinksEnabled = enabled;
552     setMouseTracking(enabled);
553     highlighter->setObjectLinksEnabled(enabled);
554     highlighter->rehighlight();
555 
556     if (enabled)
557         handleValidObjectCursor(mapFromGlobal(QCursor::pos()));
558     else
559         viewport()->setCursor(Qt::IBeamCursor);
560 }
561 
addDbObject(int from,int to,const QString & dbName)562 void SqlEditor::addDbObject(int from, int to, const QString& dbName)
563 {
564     validDbObjects << DbObject(from, to, dbName);
565     highlighter->addDbObject(from, to);
566 }
567 
clearDbObjects()568 void SqlEditor::clearDbObjects()
569 {
570     validDbObjects.clear();
571     highlighter->clearDbObjects();
572 }
573 
lineNumberAreaPaintEvent(QPaintEvent * event)574 void SqlEditor::lineNumberAreaPaintEvent(QPaintEvent* event)
575 {
576     QPainter painter(lineNumberArea);
577     painter.fillRect(event->rect(), STYLE->extendedPalette().editorLineBase());
578     QTextBlock block = firstVisibleBlock();
579     int blockNumber = block.blockNumber();
580     int top = (int) blockBoundingGeometry(block).translated(contentOffset()).top();
581     int bottom = top + (int) blockBoundingRect(block).height();
582     while (block.isValid() && top <= event->rect().bottom())
583     {
584         if (block.isVisible() && bottom >= event->rect().top())
585         {
586             QString number = QString::number(blockNumber + 1);
587             painter.setPen(style()->standardPalette().text().color());
588             painter.drawText(0, top, lineNumberArea->width()-2, fontMetrics().height(), Qt::AlignRight, number);
589         }
590 
591         block = block.next();
592         top = bottom;
593         bottom = top + (int) blockBoundingRect(block).height();
594         blockNumber++;
595     }
596 }
597 
lineNumberAreaWidth()598 int SqlEditor::lineNumberAreaWidth()
599 {
600     int digits = 1;
601     int max = qMax(1, document()->blockCount());
602     while (max >= 10)
603     {
604         max /= 10;
605         digits++;
606     }
607 
608     int space = 3 + fontMetrics().horizontalAdvance(QLatin1Char('9')) * digits;
609     return space;
610 }
611 
highlightParenthesis(QList<QTextEdit::ExtraSelection> & selections)612 void SqlEditor::highlightParenthesis(QList<QTextEdit::ExtraSelection>& selections)
613 {
614     if (!richFeaturesEnabled)
615         return;
616 
617     // Find out parenthesis under the cursor
618     int curPos = textCursor().position();
619     TextBlockData* data = dynamic_cast<TextBlockData*>(textCursor().block().userData());
620     if (!data)
621         return;
622 
623     const TextBlockData::Parenthesis* parOnRight = data->parenthesisForPosision(curPos);
624     const TextBlockData::Parenthesis* parOnLeft = data->parenthesisForPosision(curPos - 1);
625     const TextBlockData::Parenthesis* thePar = parOnRight;
626     if (parOnLeft && !parOnRight) // go with parenthesis on left only when there's no parenthesis on right
627         thePar = parOnLeft;
628 
629     if (!thePar)
630         return;
631 
632     // Getting all parenthesis in the entire document
633     QList<const TextBlockData::Parenthesis*> allParenthesis;
634     for (QTextBlock block = document()->begin(); block.isValid(); block = block.next())
635     {
636         data = dynamic_cast<TextBlockData*>(block.userData());
637         if (!data)
638             continue;
639 
640         allParenthesis += data->parentheses();
641     }
642 
643     // Matching the parenthesis
644     const TextBlockData::Parenthesis* matchedPar = matchParenthesis(allParenthesis, thePar);
645     if (!matchedPar)
646         return;
647 
648     // Mark new match
649     markMatchedParenthesis(thePar->position, matchedPar->position, selections);
650 }
651 
highlightCurrentCursorContext()652 void SqlEditor::highlightCurrentCursorContext()
653 {
654     QList<QTextEdit::ExtraSelection> selections;
655     highlightCurrentLine(selections);
656     highlightParenthesis(selections);
657     setExtraSelections(selections);
658 }
659 
markMatchedParenthesis(int pos1,int pos2,QList<QTextEdit::ExtraSelection> & selections)660 void SqlEditor::markMatchedParenthesis(int pos1, int pos2, QList<QTextEdit::ExtraSelection>& selections)
661 {
662     QTextEdit::ExtraSelection selection;
663 
664     selection.format.setBackground(style()->standardPalette().windowText());
665     selection.format.setForeground(style()->standardPalette().window());
666 
667     QTextCursor cursor = textCursor();
668 
669     cursor.setPosition(pos1);
670     cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor);
671     selection.cursor = cursor;
672     selections.append(selection);
673 
674     cursor.setPosition(pos2);
675     cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor);
676     selection.cursor = cursor;
677     selections.append(selection);
678 }
679 
doBackspace(int repeats)680 void SqlEditor::doBackspace(int repeats)
681 {
682     QTextCursor cursor = textCursor();
683     for (int i = 0; i < repeats; i++)
684         cursor.deletePreviousChar();
685 }
686 
indentSelected(bool shiftPressed)687 void SqlEditor::indentSelected(bool shiftPressed)
688 {
689     QTextCursor cursor = textCursor();
690     QTextDocument* doc = document();
691     QTextBlock startBlock = doc->findBlock(cursor.selectionStart());
692     QTextBlock endBlock = doc->findBlock(cursor.selectionEnd());
693 
694     if (cursor.selectionEnd() > endBlock.position())
695     {
696         QTextBlock afterEndBlock = endBlock.next();
697         if (afterEndBlock.isValid())
698             endBlock = afterEndBlock;
699     }
700 
701     for (QTextBlock it = startBlock; it != endBlock; it = it.next())
702     {
703         if (shiftPressed)
704             unindentBlock(it);
705         else
706             indentBlock(it);
707     }
708 }
709 
indentBlock(const QTextBlock & block)710 void SqlEditor::indentBlock(const QTextBlock& block)
711 {
712     QTextCursor cursor = textCursor();
713     cursor.setPosition(block.position());
714     cursor.insertText("    ");
715 }
716 
unindentBlock(const QTextBlock & block)717 void SqlEditor::unindentBlock(const QTextBlock& block)
718 {
719     QString str = block.text();
720     if (!str.startsWith(" "))
721         return;
722 
723     int spaces = 0;
724     int firstPrintable = str.indexOf(QRegExp("\\S"));
725     if (firstPrintable == -1)
726         spaces = str.length();
727     else
728         spaces = firstPrintable;
729 
730     QTextCursor cursor = textCursor();
731     cursor.setPosition(block.position());
732     for (int i = 0; i < 4 && i < spaces; i++)
733         cursor.deleteChar();
734 }
735 
indentNewLine()736 void SqlEditor::indentNewLine()
737 {
738     QTextCursor cursor = textCursor();
739 
740     // If there is no previous block to refer to, do nothing
741     QTextBlock previousBlock = document()->findBlockByNumber(cursor.blockNumber() - 1);
742     if (!previousBlock.isValid())
743         return;
744 
745     // If previous block has first pritable character further than current cursor position, insert spaces to meet above position
746     int previousFirstPrintable = previousBlock.text().indexOf(QRegExp("\\S"));
747     if (previousFirstPrintable > 0)
748     {
749         insertPlainText(QString(" ").repeated(previousFirstPrintable));
750         return;
751     }
752 
753 }
754 
showSearchDialog()755 void SqlEditor::showSearchDialog()
756 {
757     if (!searchDialog)
758         searchDialog = new SearchTextDialog(textLocator, this);
759 
760     if (searchDialog->isVisible())
761         searchDialog->hide();
762 
763     searchDialog->show();
764 }
765 
matchParenthesis(QList<const TextBlockData::Parenthesis * > parList,const TextBlockData::Parenthesis * thePar)766 const TextBlockData::Parenthesis* SqlEditor::matchParenthesis(QList<const TextBlockData::Parenthesis*> parList,
767                                                               const TextBlockData::Parenthesis* thePar)
768 {
769     bool matchLeftPar = (thePar->character == ')');
770     char parToMatch = matchLeftPar ? '(' : ')';
771     int parListSize = parList.size();
772     int theParIdx = parList.indexOf(thePar);
773     int counter = 0;
774     for (int i = theParIdx; (matchLeftPar ? i >= 0 : i < parListSize); (matchLeftPar ? i-- : i++))
775     {
776         if (parList[i]->character == parToMatch)
777             counter--;
778         else
779             counter++;
780 
781         if (counter == 0)
782             return parList[i];
783     }
784     return nullptr;
785 }
786 
completerTypedText(const QString & text)787 void SqlEditor::completerTypedText(const QString& text)
788 {
789     insertPlainText(text);
790     completer->extendFilterBy(text);
791     updateCompleterPosition();
792 }
793 
completerBackspacePressed()794 void SqlEditor::completerBackspacePressed()
795 {
796     deletionKeyPressed = true;
797     textCursor().deletePreviousChar();
798     completer->shringFilterBy(1);
799     updateCompleterPosition();
800     deletionKeyPressed = false;
801 }
802 
completerLeftPressed()803 void SqlEditor::completerLeftPressed()
804 {
805     completer->shringFilterBy(1);
806     moveCursor(QTextCursor::Left);
807     updateCompleterPosition();
808 }
809 
completerRightPressed()810 void SqlEditor::completerRightPressed()
811 {
812     // Last character seems to be virtual in QPlainTextEdit, so that QTextCursor can be at its position
813     int charCnt = document()->characterCount() - 1;
814     int curPos = textCursor().position();
815 
816     if (curPos >= charCnt)
817     {
818         completer->reject();
819         return;
820     }
821 
822     QChar c = document()->characterAt(curPos);
823     if (!c.isNull())
824         completer->extendFilterBy(QString(c));
825 
826     moveCursor(QTextCursor::Right);
827     updateCompleterPosition();
828 }
829 
parseContents()830 void SqlEditor::parseContents()
831 {
832     if (!richFeaturesEnabled)
833         return;
834 
835     QString sql = toPlainText();
836     if (!virtualSqlExpression.isNull())
837     {
838         if (virtualSqlCompleteSemicolon && !sql.trimmed().endsWith(";"))
839             sql += ";";
840 
841         sql = virtualSqlExpression.arg(sql);
842     }
843 
844     if (richFeaturesEnabled)
845     {
846         queryParser->parse(sql);
847         checkForValidObjects();
848         checkForSyntaxErrors();
849         highlighter->rehighlight();
850     }
851 }
852 
checkForSyntaxErrors()853 void SqlEditor::checkForSyntaxErrors()
854 {
855     syntaxValidated = true;
856 
857     removeErrorMarkers();
858 
859     // Marking invalid tokens, like in "SELECT * from test] t" - the "]" token is invalid.
860     // Such tokens don't cause parser to fail.
861     for (SqliteQueryPtr query : queryParser->getQueries())
862     {
863         for (TokenPtr token : query->tokens)
864         {
865             if (token->type == Token::INVALID)
866                 markErrorAt(token->start, token->end, true);
867         }
868     }
869 
870     if (queryParser->isSuccessful())
871     {
872         emit errorsChecked(false);
873         return;
874     }
875 
876     // Setting new markers when errors were detected
877     for (ParserError* error : queryParser->getErrors())
878         markErrorAt(sqlIndex(error->getFrom()), sqlIndex(error->getTo()));
879 
880     emit errorsChecked(true);
881 }
882 
checkForValidObjects()883 void SqlEditor::checkForValidObjects()
884 {
885     clearDbObjects();
886     if (!db || !db->isValid())
887         return;
888 
889     QMutexLocker lock(&objectsInNamedDbMutex);
890     QList<SqliteStatement::FullObject> fullObjects;
891     QString dbName;
892     for (SqliteQueryPtr query : queryParser->getQueries())
893     {
894         fullObjects = query->getContextFullObjects();
895         for (const SqliteStatement::FullObject& fullObj : fullObjects)
896         {
897             dbName = fullObj.database ? stripObjName(fullObj.database->value) : "main";
898             if (!objectsInNamedDb.contains(dbName))
899                 continue;
900 
901             if (fullObj.type == SqliteStatement::FullObject::DATABASE)
902             {
903                 // Valid db name
904                 addDbObject(sqlIndex(fullObj.database->start), sqlIndex(fullObj.database->end), QString());
905                 continue;
906             }
907 
908             if (!objectsInNamedDb[dbName].contains(stripObjName(fullObj.object->value)))
909                 continue;
910 
911             // Valid object name
912             addDbObject(sqlIndex(fullObj.object->start), sqlIndex(fullObj.object->end), dbName);
913         }
914     }
915 }
916 
scheduleQueryParser(bool force)917 void SqlEditor::scheduleQueryParser(bool force)
918 {
919     if (!document()->isModified() && !force)
920         return;
921 
922     syntaxValidated = false;
923 
924     document()->setModified(false);
925     queryParserTrigger->schedule();
926     autoCompleteTrigger->schedule();
927 }
928 
sqlIndex(int idx)929 int SqlEditor::sqlIndex(int idx)
930 {
931     if (virtualSqlExpression.isNull())
932         return idx;
933 
934     if (idx < virtualSqlOffset)
935         return virtualSqlOffset;
936 
937     idx -= virtualSqlOffset;
938 
939     int lastIdx = toPlainText().length() - 1;
940     if (idx > lastIdx)
941         return lastIdx;
942 
943     return idx;
944 }
945 
updateLineNumberArea()946 void SqlEditor::updateLineNumberArea()
947 {
948     updateLineNumberArea(viewport()->rect(), viewport()->y());
949 }
950 
hasSelection() const951 bool SqlEditor::hasSelection() const
952 {
953     return textCursor().hasSelection();
954 }
955 
replaceSelectedText(const QString & newText)956 void SqlEditor::replaceSelectedText(const QString &newText)
957 {
958     textCursor().insertText(newText);
959 }
960 
getSelectedText() const961 QString SqlEditor::getSelectedText() const
962 {
963     QString txt = textCursor().selectedText();
964     fixTextCursorSelectedText(txt);
965     return txt;
966 }
967 
openObject(const QString & database,const QString & name)968 void SqlEditor::openObject(const QString& database, const QString& name)
969 {
970     DbObjectDialogs dialogs(db);
971     dialogs.editObject(database, name);
972 }
973 
updateLineNumberAreaWidth()974 void SqlEditor::updateLineNumberAreaWidth()
975 {
976     if (!showLineNumbers)
977     {
978         setViewportMargins(0, 0, 0, 0);
979         return;
980     }
981 
982     setViewportMargins(lineNumberAreaWidth(), 0, 0, 0);
983 }
984 
highlightCurrentLine(QList<QTextEdit::ExtraSelection> & selections)985 void SqlEditor::highlightCurrentLine(QList<QTextEdit::ExtraSelection>& selections)
986 {
987     if (!isReadOnly() && isEnabled())
988     {
989         QTextEdit::ExtraSelection selection;
990 
991         selection.format.setBackground(STYLE->extendedPalette().editorLineBase());
992         selection.format.setProperty(QTextFormat::FullWidthSelection, true);
993         selection.cursor = textCursor();
994         selection.cursor.clearSelection();
995         selections.append(selection);
996     }
997 }
998 
updateLineNumberArea(const QRect & rect,int dy)999 void SqlEditor::updateLineNumberArea(const QRect& rect, int dy)
1000 {
1001     if (!showLineNumbers)
1002     {
1003         updateLineNumberAreaWidth();
1004         return;
1005     }
1006 
1007     if (dy)
1008         lineNumberArea->scroll(0, dy);
1009     else
1010         lineNumberArea->update(0, rect.y(), lineNumberArea->width(), rect.height());
1011 
1012     if (rect.contains(viewport()->rect()))
1013         updateLineNumberAreaWidth();
1014 }
1015 
cursorMoved()1016 void SqlEditor::cursorMoved()
1017 {
1018     highlightCurrentCursorContext();
1019     if (!cursorMovingByLocator)
1020     {
1021         textLocator->setStartPosition(textCursor().position());
1022         textLocator->cursorMoved();
1023     }
1024 }
1025 
checkContentSize()1026 void SqlEditor::checkContentSize()
1027 {
1028     if (document()->characterCount() > SqliteSyntaxHighlighter::MAX_QUERY_LENGTH)
1029     {
1030         if (richFeaturesEnabled)
1031             notifyWarn(tr("Contents of the SQL editor are huge, so errors detecting and existing objects highlighting are temporarily disabled."));
1032 
1033         richFeaturesEnabled = false;
1034     }
1035     else if (!richFeaturesEnabled)
1036     {
1037         richFeaturesEnabled = true;
1038     }
1039 }
1040 
formatSql()1041 void SqlEditor::formatSql()
1042 {
1043     QString sql = hasSelection() ? getSelectedText() : toPlainText();
1044     sql = SQLITESTUDIO->getCodeFormatter()->format("sql", sql, db);
1045 
1046     if (!hasSelection())
1047         selectAll();
1048 
1049     replaceSelectedText(sql);
1050 }
1051 
saveToFile()1052 void SqlEditor::saveToFile()
1053 {
1054     if (loadedFile.isNull())
1055         saveAsToFile();
1056     else
1057         saveToFile(loadedFile);
1058 }
1059 
saveAsToFile()1060 void SqlEditor::saveAsToFile()
1061 {
1062     QString dir = getFileDialogInitPath();
1063     QString fName = QFileDialog::getSaveFileName(this, tr("Save to file"), dir);
1064     if (fName.isNull())
1065         return;
1066 
1067     setFileDialogInitPathByFile(fName);
1068     loadedFile = fName;
1069     saveToFile(loadedFile);
1070 }
1071 
loadFromFile()1072 void SqlEditor::loadFromFile()
1073 {
1074     QString dir = getFileDialogInitPath();
1075     QString filters = tr("SQL scripts (*.sql);;All files (*)");
1076     QString fName = QFileDialog::getOpenFileName(this, tr("Open file"), dir, filters);
1077     if (fName.isNull())
1078         return;
1079 
1080     setFileDialogInitPathByFile(fName);
1081 
1082     QString err;
1083     QString sql = readFileContents(fName, &err);
1084     if (sql.isNull() && !err.isNull())
1085     {
1086         notifyError(tr("Could not open file '%1' for reading: %2").arg(fName).arg(err));
1087         return;
1088     }
1089 
1090     setPlainText(sql);
1091 
1092     loadedFile = fName;
1093 }
1094 
deleteLine()1095 void SqlEditor::deleteLine()
1096 {
1097     QTextCursor cursor = textCursor();
1098     if (cursor.hasSelection())
1099         deleteSelectedLines();
1100     else
1101         deleteCurrentLine();
1102 }
1103 
deleteCurrentLine()1104 void SqlEditor::deleteCurrentLine()
1105 {
1106     QTextCursor cursor = textCursor();
1107     cursor.movePosition(QTextCursor::StartOfLine);
1108     cursor.movePosition(QTextCursor::EndOfLine, QTextCursor::KeepAnchor);
1109     cursor.removeSelectedText();
1110 
1111     QTextDocument* doc = document();
1112     QTextBlock block = doc->findBlock(cursor.position());
1113     if (block.next().isValid())
1114         cursor.deleteChar();
1115     else
1116     {
1117         cursor.deletePreviousChar();
1118         cursor.movePosition(QTextCursor::StartOfLine);
1119     }
1120     setTextCursor(cursor);
1121 }
1122 
deleteSelectedLines()1123 void SqlEditor::deleteSelectedLines()
1124 {
1125     QTextCursor cursor = textCursor();
1126     QTextDocument* doc = document();
1127     QTextBlock startBlock = doc->findBlock(cursor.selectionStart());
1128     QTextBlock endBlock = doc->findBlock(cursor.selectionEnd() - 1);
1129     int idxMod = 0;
1130     if (!endBlock.next().isValid()) // no newline at the end
1131         idxMod = -1;
1132 
1133     cursor.setPosition(startBlock.position());
1134     cursor.setPosition(endBlock.position() + endBlock.length() + idxMod, QTextCursor::KeepAnchor);
1135     cursor.removeSelectedText();
1136 }
moveBlockDown(bool deleteOld)1137 void SqlEditor::moveBlockDown(bool deleteOld)
1138 {
1139     QTextCursor cursor = textCursor();
1140     if (!cursor.hasSelection())
1141     {
1142         cursor.movePosition(QTextCursor::StartOfLine);
1143         cursor.movePosition(QTextCursor::EndOfLine, QTextCursor::KeepAnchor);
1144     }
1145 
1146     QTextDocument* doc = document();
1147     QTextBlock startBlock = doc->findBlock(cursor.selectionStart());
1148     QTextBlock endBlock = doc->findBlock(cursor.selectionEnd() - 1);
1149 
1150     QTextBlock nextBlock = endBlock.next();
1151     QTextBlock blockBeforeNewText = endBlock;
1152 
1153     // When moving text, we next block to be valid and operate on one after that
1154     if (deleteOld)
1155     {
1156         if (!nextBlock.isValid())
1157             return;
1158 
1159         blockBeforeNewText = nextBlock;
1160         nextBlock = nextBlock.next();
1161     }
1162 
1163     // If next block is invalid, we need to create it
1164     bool removeLastNewLine = false;
1165     if (!nextBlock.isValid())
1166     {
1167         cursor.setPosition(blockBeforeNewText.position());
1168         cursor.movePosition(QTextCursor::EndOfLine);
1169         cursor.insertBlock();
1170         nextBlock = blockBeforeNewText.next();
1171         removeLastNewLine = true;
1172     }
1173 
1174     int textLength = endBlock.position() + endBlock.length() - startBlock.position();
1175 
1176     // Collecting text and removing text from old position (if not copying)
1177     cursor.setPosition(startBlock.position());
1178     cursor.setPosition(startBlock.position() + textLength, QTextCursor::KeepAnchor);
1179     QString text = cursor.selectedText();
1180     fixTextCursorSelectedText(text);
1181     if (deleteOld) // this is false when just copying
1182         cursor.removeSelectedText();
1183 
1184     // Pasting text at new position and reselecting it
1185     cursor.setPosition(nextBlock.position());
1186     cursor.insertText(text);
1187     cursor.setPosition(nextBlock.position() + textLength);
1188     if (removeLastNewLine) // this is done when we moved to the last line, created block and copied another \n to it
1189         cursor.deletePreviousChar();
1190 
1191     cursor.setPosition(nextBlock.position(), QTextCursor::KeepAnchor);
1192     setTextCursor(cursor);
1193 }
1194 
moveBlockUp(bool deleteOld)1195 void SqlEditor::moveBlockUp(bool deleteOld)
1196 {
1197     QTextCursor cursor = textCursor();
1198     if (!cursor.hasSelection())
1199     {
1200         cursor.movePosition(QTextCursor::StartOfLine);
1201         cursor.movePosition(QTextCursor::EndOfLine, QTextCursor::KeepAnchor);
1202     }
1203 
1204     QTextDocument* doc = document();
1205     QTextBlock startBlock = doc->findBlock(cursor.selectionStart());
1206     QTextBlock endBlock = doc->findBlock(cursor.selectionEnd() - 1);
1207     bool hasNewLineChar = endBlock.next().isValid();
1208 
1209     QTextBlock insertingBlock = startBlock;
1210     if (deleteOld)
1211     {
1212         insertingBlock = startBlock.previous();
1213         if (!insertingBlock.isValid())
1214             return;
1215     }
1216 
1217     // We will operate on full line length, unless next block was invalid, thus at the end there's no new line.
1218     int textLength = endBlock.position() + endBlock.length() - startBlock.position();
1219     if (!hasNewLineChar)
1220         textLength--;
1221 
1222     // Collecting text and removing text from old position (if not copying)
1223     cursor.setPosition(startBlock.position());
1224     cursor.setPosition(startBlock.position() + textLength, QTextCursor::KeepAnchor);
1225     QString text = cursor.selectedText();
1226     fixTextCursorSelectedText(text);
1227     if (deleteOld) // this is false when just copying
1228         cursor.removeSelectedText();
1229 
1230     // Pasting text at new position
1231     cursor.setPosition(insertingBlock.position());
1232     cursor.insertText(text);
1233     if (!hasNewLineChar)
1234     {
1235         cursor.insertBlock();
1236         cursor.setPosition(insertingBlock.next().next().position());
1237         cursor.deletePreviousChar();
1238         textLength++; // we will need to include "new line" when reselecting text
1239     }
1240 
1241     // Reselecting new text
1242     cursor.setPosition(insertingBlock.position() + textLength);
1243     cursor.setPosition(insertingBlock.position(), QTextCursor::KeepAnchor);
1244     setTextCursor(cursor);
1245 }
1246 
copyBlockDown()1247 void SqlEditor::copyBlockDown()
1248 {
1249     moveBlockDown(false);
1250 }
1251 
copyBlockUp()1252 void SqlEditor::copyBlockUp()
1253 {
1254     moveBlockUp(false);
1255 }
1256 
find()1257 void SqlEditor::find()
1258 {
1259     textLocator->setStartPosition(textCursor().position());
1260     showSearchDialog();
1261 }
1262 
findNext()1263 void SqlEditor::findNext()
1264 {
1265     textLocator->findNext();
1266 }
1267 
findPrevious()1268 void SqlEditor::findPrevious()
1269 {
1270     textLocator->findPrev();
1271 }
1272 
replace()1273 void SqlEditor::replace()
1274 {
1275     showSearchDialog();
1276 }
1277 
found(int start,int end)1278 void SqlEditor::found(int start, int end)
1279 {
1280     QTextCursor cursor = textCursor();
1281     cursor.setPosition(end);
1282     cursor.setPosition(start, QTextCursor::KeepAnchor);
1283     cursorMovingByLocator = true;
1284     setTextCursor(cursor);
1285     cursorMovingByLocator = false;
1286     ensureCursorVisible();
1287 }
1288 
reachedEnd()1289 void SqlEditor::reachedEnd()
1290 {
1291     notifyInfo(tr("Reached the end of document. Hit the find again to restart the search."));
1292 }
1293 
changeFont(const QVariant & font)1294 void SqlEditor::changeFont(const QVariant& font)
1295 {
1296     setFont(font.value<QFont>());
1297 }
1298 
configModified()1299 void SqlEditor::configModified()
1300 {
1301     highlighter->rehighlight();
1302     highlightCurrentCursorContext();
1303 }
1304 
toggleComment()1305 void SqlEditor::toggleComment()
1306 {
1307     // Handle no selection - toggle single line
1308     QTextCursor cur = textCursor();
1309     int start = cur.selectionStart();
1310     int end = cur.selectionEnd();
1311 
1312     if (start == end)
1313     {
1314         toggleLineCommentForLine(cur.block());
1315         return;
1316     }
1317 
1318     // Handle multiline selection - from begin of the line to begin of the line
1319     QTextDocument* doc = document();
1320 
1321     QTextBlock startBlock = doc->findBlock(start);
1322     bool startAtLineBegining = startBlock.position() == start;
1323 
1324     QTextBlock endBlock = doc->findBlock(end);
1325     bool endAtLineBegining = endBlock.position() == end;
1326 
1327     if (startAtLineBegining && endAtLineBegining)
1328     {
1329         // Check if all lines where commented previously
1330         bool allCommented = true;
1331         for (QTextBlock theBlock = startBlock; theBlock != endBlock; theBlock = theBlock.next())
1332         {
1333             if (!theBlock.text().startsWith("--"))
1334             {
1335                 allCommented = false;
1336                 break;
1337             }
1338         }
1339 
1340         // Apply comment toggle
1341         cur.beginEditBlock();
1342         for (QTextBlock theBlock = startBlock; theBlock != endBlock; theBlock = theBlock.next())
1343         {
1344             cur.setPosition(theBlock.position());
1345             if (allCommented)
1346             {
1347                 cur.deleteChar();
1348                 cur.deleteChar();
1349             }
1350             else
1351                 cur.insertText("--");
1352         }
1353 
1354         cur.setPosition(start);
1355         cur.setPosition(endBlock.position(), QTextCursor::KeepAnchor);
1356         cur.endEditBlock();
1357         setTextCursor(cur);
1358         return;
1359     }
1360 
1361     // Handle custom selection
1362     QString txt = cur.selectedText().trimmed();
1363     cur.beginEditBlock();
1364     if (txt.startsWith("/*") && txt.endsWith("*/"))
1365     {
1366         cur.setPosition(end);
1367         cur.deletePreviousChar();
1368         cur.deletePreviousChar();
1369         cur.setPosition(start);
1370         cur.deleteChar();
1371         cur.deleteChar();
1372 
1373         cur.setPosition(start);
1374         cur.setPosition(end - 4, QTextCursor::KeepAnchor);
1375     }
1376     else
1377     {
1378         cur.setPosition(end);
1379         cur.insertText("*/");
1380         cur.setPosition(start);
1381         cur.insertText("/*");
1382 
1383         cur.setPosition(start);
1384         cur.setPosition(end + 4, QTextCursor::KeepAnchor);
1385     }
1386     cur.endEditBlock();
1387     setTextCursor(cur);
1388 }
1389 
keyPressEvent(QKeyEvent * e)1390 void SqlEditor::keyPressEvent(QKeyEvent* e)
1391 {
1392     switch (e->key())
1393     {
1394         case Qt::Key_Backspace:
1395         {
1396             deletionKeyPressed = true;
1397             if (e->modifiers().testFlag(Qt::NoModifier))
1398                 backspacePressed();
1399             else
1400                 QPlainTextEdit::keyPressEvent(e);
1401             deletionKeyPressed = false;
1402             return;
1403         }
1404         case Qt::Key_Delete:
1405         {
1406             deletionKeyPressed = true;
1407             QPlainTextEdit::keyPressEvent(e);
1408             deletionKeyPressed = false;
1409             return;
1410         }
1411         case Qt::Key_Home:
1412         {
1413             homePressed(e->modifiers());
1414             return;
1415         }
1416         case Qt::Key_Tab:
1417         {
1418             tabPressed(e->modifiers().testFlag(Qt::ShiftModifier));
1419             return;
1420         }
1421         case Qt::Key_Backtab:
1422         {
1423             tabPressed(true);
1424             return;
1425         }
1426         case Qt::Key_Return:
1427         case Qt::Key_Enter:
1428         {
1429             QPlainTextEdit::keyPressEvent(e);
1430             indentNewLine();
1431             return;
1432         }
1433         case Qt::Key_Control:
1434             setObjectLinks(true);
1435             break;
1436         default:
1437             break;
1438     }
1439     QPlainTextEdit::keyPressEvent(e);
1440 }
1441 
keyReleaseEvent(QKeyEvent * e)1442 void SqlEditor::keyReleaseEvent(QKeyEvent* e)
1443 {
1444     if (e->key() == Qt::Key_Control)
1445         setObjectLinks(false);
1446 
1447     QPlainTextEdit::keyReleaseEvent(e);
1448 }
1449 
focusOutEvent(QFocusEvent * e)1450 void SqlEditor::focusOutEvent(QFocusEvent* e)
1451 {
1452     UNUSED(e);
1453     setObjectLinks(false);
1454     QPlainTextEdit::focusOutEvent(e);
1455 }
1456 
focusInEvent(QFocusEvent * e)1457 void SqlEditor::focusInEvent(QFocusEvent* e)
1458 {
1459     if (completer->isVisible())
1460     {
1461         // Sometimes, when switching to other application window and then getting back to SQLiteStudio,
1462         // the completer loses focus, but doesn't close. In that case, the SqlEditor gets focused,
1463         // while completer still exists. Here we fix this case.
1464         completer->reject();
1465         return;
1466     }
1467 
1468     QPlainTextEdit::focusInEvent(e);
1469 }
1470 
mouseMoveEvent(QMouseEvent * e)1471 void SqlEditor::mouseMoveEvent(QMouseEvent* e)
1472 {
1473     handleValidObjectCursor(e->pos());
1474     QPlainTextEdit::mouseMoveEvent(e);
1475 }
1476 
mousePressEvent(QMouseEvent * e)1477 void SqlEditor::mousePressEvent(QMouseEvent* e)
1478 {
1479     if (objectLinksEnabled)
1480     {
1481         const DbObject* obj = getValidObjectForPosition(e->pos());
1482         if (obj && e->button() == Qt::LeftButton)
1483         {
1484             QString objName = toPlainText().mid(obj->from, (obj->to - obj->from + 1));
1485             openObject(obj->dbName, stripObjName(objName));
1486         }
1487     }
1488 
1489     QPlainTextEdit::mousePressEvent(e);
1490 }
1491 
resizeEvent(QResizeEvent * e)1492 void SqlEditor::resizeEvent(QResizeEvent* e)
1493 {
1494     QPlainTextEdit::resizeEvent(e);
1495     QRect cr = contentsRect();
1496     lineNumberArea->setGeometry(QRect(cr.left(), cr.top(), lineNumberAreaWidth(), cr.height()));
1497 }
1498 
handleValidObjectCursor(const QPoint & point)1499 void SqlEditor::handleValidObjectCursor(const QPoint& point)
1500 {
1501     if (!objectLinksEnabled)
1502         return;
1503 
1504     QTextCursor cursor = cursorForPosition(point);
1505     int position = cursor.position();
1506     QRect curRect = cursorRect(cursor);
1507     bool isValid = false;
1508     if (point.y() >= curRect.top() && point.y() <= curRect.bottom())
1509     {
1510         // Mouse pointer is at the same line as cursor, so cursor was returned for actual character under mouse
1511         // and not just first/last character of the line, because mouse was out of text.
1512         bool movedLeft = (curRect.x() - point.x()) < 0;
1513         isValid = (getValidObjectForPosition(position, movedLeft) != nullptr);
1514     }
1515     viewport()->setCursor(isValid ? Qt::PointingHandCursor : Qt::IBeamCursor);
1516 }
1517 
getVirtualSqlCompleteSemicolon() const1518 bool SqlEditor::getVirtualSqlCompleteSemicolon() const
1519 {
1520     return virtualSqlCompleteSemicolon;
1521 }
1522 
setVirtualSqlCompleteSemicolon(bool value)1523 void SqlEditor::setVirtualSqlCompleteSemicolon(bool value)
1524 {
1525     virtualSqlCompleteSemicolon = value;
1526 }
1527 
getShowLineNumbers() const1528 bool SqlEditor::getShowLineNumbers() const
1529 {
1530     return showLineNumbers;
1531 }
1532 
setShowLineNumbers(bool value)1533 void SqlEditor::setShowLineNumbers(bool value)
1534 {
1535     showLineNumbers = value;
1536     lineNumberArea->setVisible(value);
1537     updateLineNumberArea();
1538 }
1539 
checkSyntaxNow()1540 void SqlEditor::checkSyntaxNow()
1541 {
1542     queryParserTrigger->cancel();
1543     parseContents();
1544 }
1545 
saveSelection()1546 void SqlEditor::saveSelection()
1547 {
1548     QTextCursor cur = textCursor();
1549     storedSelectionStart = cur.selectionStart();
1550     storedSelectionEnd = cur.selectionEnd();
1551 }
1552 
restoreSelection()1553 void SqlEditor::restoreSelection()
1554 {
1555     QTextCursor cur = textCursor();
1556     cur.setPosition(storedSelectionStart);
1557     cur.setPosition(storedSelectionEnd, QTextCursor::KeepAnchor);
1558 }
1559 
getToolBar(int toolbar) const1560 QToolBar* SqlEditor::getToolBar(int toolbar) const
1561 {
1562     UNUSED(toolbar);
1563     return nullptr;
1564 }
1565 
getVirtualSqlExpression() const1566 QString SqlEditor::getVirtualSqlExpression() const
1567 {
1568     return virtualSqlExpression;
1569 }
1570 
setVirtualSqlExpression(const QString & value)1571 void SqlEditor::setVirtualSqlExpression(const QString& value)
1572 {
1573     virtualSqlExpression = value;
1574 
1575     virtualSqlOffset = virtualSqlExpression.indexOf("%1");
1576     if (virtualSqlOffset == -1)
1577     {
1578         virtualSqlOffset = 0;
1579         virtualSqlExpression = QString();
1580         qWarning() << "Tried to set invalid virtualSqlExpression for SqlEditor. Ignored.";
1581         return;
1582     }
1583 
1584     virtualSqlRightOffset = virtualSqlExpression.length() - virtualSqlOffset - 2;
1585 }
1586 
setTriggerContext(const QString & table)1587 void SqlEditor::setTriggerContext(const QString& table)
1588 {
1589     createTriggerTable = table;
1590     highlighter->setCreateTriggerContext(!table.isEmpty());
1591 }
1592 
getValidObjectForPosition(const QPoint & point)1593 const SqlEditor::DbObject* SqlEditor::getValidObjectForPosition(const QPoint& point)
1594 {
1595     QTextCursor cursor = cursorForPosition(point);
1596     int position = cursor.position();
1597     bool movedLeft = (cursorRect(cursor).x() - point.x()) < 0;
1598     return getValidObjectForPosition(position, movedLeft);
1599 }
1600 
getValidObjectForPosition(int position,bool movedLeft)1601 const SqlEditor::DbObject* SqlEditor::getValidObjectForPosition(int position, bool movedLeft)
1602 {
1603     for (const DbObject& obj : validDbObjects)
1604     {
1605         if ((!movedLeft && position > obj.from && position-1 <= obj.to) ||
1606             (movedLeft && position >= obj.from && position <= obj.to))
1607         {
1608             return &obj;
1609         }
1610     }
1611     return nullptr;
1612 }
1613 
DbObject(int from,int to,const QString & dbName)1614 SqlEditor::DbObject::DbObject(int from, int to, const QString& dbName) :
1615     from(from), to(to), dbName(dbName)
1616 {
1617 
1618 }
1619 
LineNumberArea(SqlEditor * editor)1620 SqlEditor::LineNumberArea::LineNumberArea(SqlEditor* editor) :
1621     QWidget(editor), codeEditor(editor)
1622 {
1623 }
1624 
sizeHint() const1625 QSize SqlEditor::LineNumberArea::sizeHint() const
1626 {
1627     return QSize(codeEditor->lineNumberAreaWidth(), 0);
1628 }
1629 
paintEvent(QPaintEvent * event)1630 void SqlEditor::LineNumberArea::paintEvent(QPaintEvent* event)
1631 {
1632     if (codeEditor->getShowLineNumbers())
1633         codeEditor->lineNumberAreaPaintEvent(event);
1634 }
1635 
1636 
changeEvent(QEvent * e)1637 void SqlEditor::changeEvent(QEvent* e)
1638 {
1639 //    if (e->type() == QEvent::EnabledChange)
1640 //        highlightCurrentLine();
1641 
1642     QPlainTextEdit::changeEvent(e);
1643 }
1644