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