/** * ScriptTextEditor.cpp * Copyright (c) 2013 ROBLOX Corp. All Rights Reserved. */ #include "stdafx.h" #include "ScriptTextEditor.h" // Qt Headers #include #include // Roblox Headers #include "script/ScriptContext.h" #include "v8datamodel/DataModel.h" // Roblox Studio Headers #include "RobloxScriptDoc.h" #include "RobloxMainWindow.h" #include "AuthoringSettings.h" #include "ScriptSyntaxHighlighter.h" #include "ScriptSideWidget.h" #include "FindDialog.h" #include "UpdateUIManager.h" #include "QtUtilities.h" #include "StudioIntellesense.h" #include "RobloxDocManager.h" #include "ScriptAnalysisWidget.h" #include "RobloxCustomWidgets.h" FASTFLAG(LuaDebugger) FASTFLAG(StudioSeparateActionByActivationMethod) FASTFLAGVARIABLE(ScriptContextSearchFix, true) static const int MaxFontZoomIn = 30; static const int MaxFontZoomOut = 5; static const int WikiSearchTimerInterval = 1000; ScriptTextEditor::WikiLookup ScriptTextEditor::wikiLookup = WikiLookup(""); FASTINTVARIABLE(StudioSyntaxCheckTimerInterval, 500) FASTFLAGVARIABLE(ElseAutoIndentFix, false) FASTFLAGVARIABLE(StudioEmbeddedFindDialogEnabled, false) // required for QList static bool operator== (const QTextEdit::ExtraSelection& lhs, const QTextEdit::ExtraSelection& rhs) { return (lhs.cursor == rhs.cursor && lhs.format == rhs.format); } // TODO - auto-format selected text /*****************************************************************************/ // ScriptTextEditor /*****************************************************************************/ ScriptTextEditor::ScriptTextEditor( RobloxScriptDoc* pScriptDoc, RobloxMainWindow* pMainWindow) : QPlainTextEdit(pMainWindow), m_pScriptDoc(pScriptDoc), m_pSyntaxHighlighter(NULL), m_pSideWidget(NULL), m_pCheckSyntaxThread(NULL), m_pFindThread(NULL), m_pCheckSyntaxTimer(NULL), m_errorLine(-1), m_pMainWindow(pMainWindow), currentWikiSearchSelection(), m_selectionMutex(QMutex::Recursive) { // highlighter m_pSyntaxHighlighter = new ScriptSyntaxHighlighter(document()); m_pSideWidget = new ScriptSideWidget(this); connect(this,SIGNAL(updateRequest(QRect,int)),m_pSideWidget,SLOT(updateArea())); if (FFlag::LuaDebugger) connect(m_pSideWidget, SIGNAL(toggleBreakpoint(int)), this, SIGNAL(toggleBreakpoint(int))); m_pCheckSyntaxThread = new CheckSyntaxThread(this, pScriptDoc); connect(this,SIGNAL(textChanged()),this,SLOT(checkSyntax())); if (FFlag::StudioEmbeddedFindDialogEnabled) { m_pFindThread = new FindThread(this, pScriptDoc); connect(this, SIGNAL(textChanged()), this, SLOT(startFind())); } m_pCheckSyntaxTimer = new QTimer(this); connect(m_pCheckSyntaxTimer,SIGNAL(timeout()),this,SLOT(onCheckSyntaxTimerTimeOut())); setFrameShape(QFrame::NoFrame); setFrameShadow(QFrame::Plain); setMouseTracking(true); // always show scroll bars - avoids flickering // (-- Not too sure when does this happen? disabling for ribbon) if (!m_pMainWindow->isRibbonStyle()) { setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOn); setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn); } // listen for changes to the editor settings m_PropertyChangedConnection = AuthoringSettings::singleton().propertyChangedSignal.connect( boost::bind(&ScriptTextEditor::onPropertyChanged,this,_1)); onPropertyChanged(NULL); m_ContextMenu = new QMenu(this); { m_ContextMenu->addAction(m_pMainWindow->undoAction); m_ContextMenu->addAction(m_pMainWindow->redoAction); m_ContextMenu->addSeparator(); m_ContextMenu->addAction(m_pMainWindow->cutAction); m_ContextMenu->addAction(m_pMainWindow->copyAction); m_ContextMenu->addAction(m_pMainWindow->pasteAction); m_ContextMenu->addAction(m_pMainWindow->deleteSelectedAction); m_ContextMenu->addSeparator(); m_ContextMenu->addAction(m_pMainWindow->selectAllAction); m_ContextMenu->addAction(m_pMainWindow->goToScriptErrorAction); m_ContextMenu->addSeparator(); m_ContextMenu->addAction(m_pMainWindow->commentSelectionAction); m_ContextMenu->addAction(m_pMainWindow->uncommentSelectionAction); m_ContextMenu->addAction(m_pMainWindow->toggleCommentAction); m_ContextMenu->addSeparator(); m_ContextMenu->addAction(m_pMainWindow->expandAllFoldsAction); m_ContextMenu->addAction(m_pMainWindow->collapseAllFoldsAction); m_ContextMenu->addSeparator(); m_ContextMenu->addAction(m_pMainWindow->zoomInAction); m_ContextMenu->addAction(m_pMainWindow->zoomOutAction); m_ContextMenu->addAction(m_pMainWindow->resetScriptZoomAction); } connect(this, SIGNAL(selectionChanged()), this, SLOT(selectionChangedSearch())); connect(this, SIGNAL(helpTopicChanged(const QString&)), &RobloxContextualHelpService::singleton(), SLOT(onHelpTopicChanged(const QString&))); m_pWikiSearchTimer = new QTimer(this); m_pWikiSearchTimer->setSingleShot(true); connect(m_pWikiSearchTimer, SIGNAL(timeout()), this, SLOT(onSearchTimeout())); } ScriptTextEditor::~ScriptTextEditor() { if (m_pCheckSyntaxThread) { delete m_pCheckSyntaxThread; m_pCheckSyntaxThread = NULL; } delete m_pWikiSearchTimer; m_PropertyChangedConnection.disconnect(); } void ScriptTextEditor::contextMenuEvent(QContextMenuEvent* event) { UpdateUIManager::Instance().updateToolBars(); if (FFlag::LuaDebugger) Q_EMIT updateContextualMenu(m_ContextMenu, event->pos()); m_ContextMenu->exec(event->globalPos()); } void ScriptTextEditor::focusInEvent(QFocusEvent* evt) { QPlainTextEdit::focusInEvent(evt); RobloxDocManager::Instance().setCurrentDoc(m_pScriptDoc); if (AuthoringSettings::singleton().intellisenseEnabled) connect(&Studio::Intellesense::singleton(), SIGNAL(doubleClickSignal(QListWidgetItem*)), this, SLOT(intellesenseDoubleClick(QListWidgetItem*))); } void ScriptTextEditor::focusOutEvent(QFocusEvent* evt) { QPlainTextEdit::focusOutEvent(evt); if (m_pScriptDoc) m_pScriptDoc->applyEditChanges(); if (AuthoringSettings::singleton().intellisenseEnabled) disconnect(&Studio::Intellesense::singleton(), SIGNAL(doubleClickSignal(QListWidgetItem*)), this, SLOT(intellesenseDoubleClick(QListWidgetItem*))); } void ScriptTextEditor::intellesenseDoubleClick(QListWidgetItem* listItem) { QString blockText = document()->findBlockByNumber(textCursor().blockNumber()).text(); int cursorPos = textCursor().positionInBlock(); Studio::Intellesense::singleton().replaceTextWithCurrentItem(blockText, cursorPos); replaceText(blockText, cursorPos); } void ScriptTextEditor::replaceText(QString text, int cursorPos) { //replace text with blockText QTextCursor cursor = textCursor(); cursor.beginEditBlock(); cursor.movePosition(QTextCursor::EndOfBlock); cursor.movePosition(QTextCursor::StartOfBlock, QTextCursor::KeepAnchor); cursor.removeSelectedText(); cursor.insertText(text); //Setting cursor position cursor.setPosition(textCursor().position() - textCursor().positionInBlock() + cursorPos); cursor.endEditBlock(); this->setTextCursor(cursor); } void ScriptTextEditor::setViewportMargins( int left, int top, int right, int bottom) { QPlainTextEdit::setViewportMargins(left,top,right,bottom); } QTextBlock ScriptTextEditor::firstVisibleBlock() const { return QPlainTextEdit::firstVisibleBlock(); } QPointF ScriptTextEditor::contentOffset() const { return QPlainTextEdit::contentOffset(); } QRectF ScriptTextEditor::blockBoundingGeometry(const QTextBlock& block) const { return QPlainTextEdit::blockBoundingGeometry(block); } void ScriptTextEditor::expandAllFolds(bool expand) { m_pSideWidget->toggleAllFolds(expand); } void ScriptTextEditor::resizeEvent(QResizeEvent* e) { QPlainTextEdit::resizeEvent(e); m_pSideWidget->setFixedHeight(height()); if (FFlag::StudioEmbeddedFindDialogEnabled) updateEmbeddedFindPosition(); } void ScriptTextEditor::updateEmbeddedFindPosition() { if (FFlag::StudioEmbeddedFindDialogEnabled) FindReplaceProvider::instance().getEmbeddedFindDialog()->updateFindPosition(mapToGlobal(viewport()->pos()), viewport()->size()); } void ScriptTextEditor::homeEvent(QKeyEvent* e) { QTextCursor currentCursor = textCursor(); QString left = currentCursor.block().text().left(currentCursor.positionInBlock()); int moveOffset = left.indexOf(QRegExp("\\S")); if (moveOffset < 0) currentCursor.movePosition(QTextCursor::StartOfBlock, (e->modifiers() == Qt::ShiftModifier) ? QTextCursor::KeepAnchor : QTextCursor::MoveAnchor); else currentCursor.setPosition(currentCursor.block().position() + moveOffset, (e->modifiers() == Qt::ShiftModifier) ? QTextCursor::KeepAnchor : QTextCursor::MoveAnchor); setTextCursor(currentCursor); } void ScriptTextEditor::endEvent(QKeyEvent* e) { QTextCursor currentCursor = textCursor(); QString right = currentCursor.block().text().right(currentCursor.block().text().length() - currentCursor.positionInBlock()); int moveOffset = right.lastIndexOf(QRegExp("\\S")); if (moveOffset < 0) currentCursor.movePosition(QTextCursor::EndOfBlock, (e->modifiers() == Qt::ShiftModifier) ? QTextCursor::KeepAnchor : QTextCursor::MoveAnchor); else currentCursor.setPosition(currentCursor.position() + moveOffset + 1, (e->modifiers() == Qt::ShiftModifier) ? QTextCursor::KeepAnchor : QTextCursor::MoveAnchor); setTextCursor(currentCursor); } bool ScriptTextEditor::event(QEvent* e) { if (e->type() == QEvent::ToolTip) { processTooltip(static_cast(e)); return true; } else if (e->type() == QEvent::KeyPress) { if (static_cast(e)->modifiers().testFlag(Qt::ControlModifier) && (static_cast(e)->key() == Qt::Key_Tab)) { e->accept(); QCoreApplication::sendEvent(&UpdateUIManager::Instance().getMainWindow(), e); return true; } } else if (e->type() == EXTRASELECTION_UPDATE) { updateFindSelection(); } return QPlainTextEdit::event(e); } void ScriptTextEditor::mousePressEvent(QMouseEvent *e) { QPlainTextEdit::mousePressEvent(e); QTextCursor cursor = textCursor(); m_originalCursorPosition = cursor.position(); } void ScriptTextEditor::keyPressEvent(QKeyEvent* e) { if (!e || !document() || isReadOnly()) return; const int key = e->key(); Qt::KeyboardModifiers modifiers = e->modifiers(); if (AuthoringSettings::singleton().intellisenseEnabled && modifiers != Qt::ControlModifier) { //Check if there is selection and key is alphanumeric or special if (textCursor().hasSelection() && key < Qt::Key_nobreakspace) textCursor().removeSelectedText(); int blockNumber = textCursor().blockNumber(); QString blockText = document()->findBlockByNumber(blockNumber).text(); int cursorPos = textCursor().positionInBlock(); QRect cursorGeometry = cursorRect(); QPoint topLeftCursorPos = mapToGlobal(cursorGeometry.topLeft()); QFontMetrics fontMetrics = QFontMetrics(font()); int cursorHeight = cursorGeometry.height(); cursorGeometry.setX(topLeftCursorPos.x() + (fontMetrics.averageCharWidth() * 2) + 40); #ifdef __APPLE__ cursorGeometry.setX(cursorGeometry.x() + 5); #endif cursorGeometry.setY(topLeftCursorPos.y()); cursorGeometry.setWidth(1); cursorGeometry.setHeight(cursorHeight); if (Studio::Intellesense::singleton().activate(blockText, blockNumber, cursorPos, e, cursorGeometry, false, this, m_analysisResult)) { if (blockText != document()->findBlockByNumber(blockNumber).text()) replaceText(blockText, cursorPos); return; } } else if (key != Qt::Key_Control && modifiers == Qt::ControlModifier) { Studio::Intellesense::singleton().deactivate(); } if ((key == Qt::Key_Tab) || (key == Qt::Key_Backtab)) { if (tabSelection(e->key() != Qt::Key_Backtab)) { e->accept(); return; } else if ( key == Qt::Key_Backtab ) { int pos = textCursor().position() - 1; QString text = toPlainText(); if ( pos > 0 && toPlainText()[pos] == '\t' ) { textCursor().deletePreviousChar(); e->accept(); return; } } } else if (key == Qt::Key_Insert) { setOverwriteMode(!overwriteMode()); e->accept(); return; } else if (key == Qt::Key_Home && modifiers != Qt::ControlModifier ) { homeEvent(e); e->accept(); return; } else if (key == Qt::Key_End && modifiers != Qt::ControlModifier ) { endEvent(e); e->accept(); return; } else if ( key == Qt::Key_Return || key == Qt::Key_Backspace ) { // Treat Shift+Return as only Return (DE5218, DE5215) if ( key == Qt::Key_Return && modifiers == Qt::ShiftModifier ) e->setModifiers(Qt::NoModifier); QTextCursor currentTextCursor = textCursor(); RBXASSERT(!currentTextCursor.isNull()); QTextBlock startBlock = currentTextCursor.block(); RBXASSERT(startBlock.isValid()); RBXASSERT(startBlock.userData()); RBXTextUserData& userData = static_cast(*startBlock.userData()); QTextBlock endBlock = ScriptEditorUtils::findFoldBoundary(startBlock,false); // if the block is folded and the block ending is valid, unfold if necessary if ( (userData.getFoldState() > 0) && endBlock.isValid() && (startBlock != endBlock) && !endBlock.isVisible() ) { // if cursor is at the end then just set it at the end of end block if ( key == Qt::Key_Return && !currentTextCursor.hasSelection() && currentTextCursor.atBlockEnd() ) { currentTextCursor.setPosition(endBlock.position() + endBlock.length() - 1); setTextCursor(currentTextCursor); } const int lineNumber = startBlock.blockNumber() + 1; m_pSideWidget->unfold(lineNumber); e->accept(); return; } // if we didn't unfold then we should auto indent next line else if ( key == Qt::Key_Return && AuthoringSettings::singleton().editorAutoIndent ) { QPlainTextEdit::keyPressEvent(e); autoIndent(); e->accept(); return; } } else if (FFlag::LuaDebugger && key == Qt::Key_F9) { QTextBlock block = textCursor().block(); if (block.isValid()) { //let debugger handle it Q_EMIT toggleBreakpoint(block.blockNumber()); e->accept(); return; } } QPlainTextEdit::keyPressEvent(e); } void ScriptTextEditor::wheelEvent(QWheelEvent* event) { // if mouse scroll wheel + control => zoom text if ( event->modifiers() & Qt::ControlModifier ) { if ( event->delta() > 0 ) zoom(1); else if ( event->delta() < 0 ) zoom(-1); m_pMainWindow->zoomInAction->setEnabled(AuthoringSettings::singleton().editorFont.pointSize() < MaxFontZoomIn); m_pMainWindow->zoomOutAction->setEnabled(AuthoringSettings::singleton().editorFont.pointSize() > MaxFontZoomOut); event->accept(); } else { QPlainTextEdit::wheelEvent(event); } } void ScriptTextEditor::zoom(int delta, bool setDeltaAsValue) { QFont font = AuthoringSettings::singleton().editorFont; int fontSize = setDeltaAsValue ? delta : qBound(MaxFontZoomOut, font.pointSize() + delta, MaxFontZoomIn); font.setPointSize(fontSize); AuthoringSettings::singleton().setEditorFont(font); } bool ScriptTextEditor::tabSelection(bool bPrepend) { bool eventAccepted = false; QTextCursor cursor = textCursor(); if (!cursor.isNull() && cursor.hasSelection()) { int posAnchor = cursor.anchor(); int pos = cursor.position(); bool bDownwardSelection = (posAnchor < pos); int startPos = cursor.selectionStart(); int endPos = cursor.selectionEnd(); QTextBlock startBlock = document()->findBlock(startPos); QTextBlock endBlock = document()->findBlock(endPos); if (startBlock.blockNumber() != endBlock.blockNumber()) { bool bEndInMid = (endPos > endBlock.position()); if (!bEndInMid) endBlock = endBlock.previous(); QString modifiedText; QTextBlock tmpBlock = startBlock; while (tmpBlock.isValid() && tmpBlock.blockNumber() <= endBlock.blockNumber()) { if (bPrepend) { modifiedText.append("\t"); modifiedText.append(tmpBlock.text()); } else { QString tmpStr = tmpBlock.text(); if (tmpStr.startsWith("\t")) { tmpStr.remove(0,1); } else if (tmpStr.startsWith(" ")) { for (int ii = 0 ; ii < AuthoringSettings::singleton().editorTabWidth ; ii++) { if (tmpStr.startsWith(" ")) tmpStr.remove(0,1); } } modifiedText.append(tmpStr); } modifiedText.append("\n"); bool isLastBlock = (tmpBlock == document()->lastBlock()); if (isLastBlock || (!bDownwardSelection && bEndInMid && (tmpBlock.blockNumber() == endBlock.blockNumber()))) modifiedText.remove(modifiedText.length() - 1,1); // Remove the extra "\n" if (isLastBlock) break; tmpBlock = tmpBlock.next(); } if (!modifiedText.isEmpty()) { replaceSelection(startPos,endPos,modifiedText); eventAccepted = true; } } } return eventAccepted; } void ScriptTextEditor::replaceSelection( int startPos, int endPos, const QString& text) { if (text.isEmpty()) return; QTextCursor cursor = textCursor(); const QTextBlock startBlock = document()->findBlock(startPos); QTextBlock endBlock = document()->findBlock(endPos); const int anchorPos = cursor.anchor(); const int currentPos = cursor.position(); const bool isDownwards = (anchorPos <= currentPos); const bool isEndInMid = (endPos > endBlock.position()); bool isLastBlock = false; if (!isEndInMid) endBlock = endBlock.previous(); // Correct the selection which may be incomplete if (isDownwards) { cursor.setPosition(anchorPos); if (anchorPos > startBlock.position()) cursor.movePosition(QTextCursor::StartOfBlock,QTextCursor::MoveAnchor); if (endBlock != document()->lastBlock()) { if (currentPos > endBlock.position()) cursor.setPosition(endBlock.next().position(),QTextCursor::KeepAnchor); else cursor.setPosition(currentPos,QTextCursor::KeepAnchor); cursor.movePosition(QTextCursor::StartOfBlock,QTextCursor::KeepAnchor); } else { cursor.setPosition(currentPos,QTextCursor::KeepAnchor); cursor.movePosition(QTextCursor::EndOfBlock,QTextCursor::KeepAnchor); isLastBlock = true; } } else { cursor.setPosition(anchorPos); if ((anchorPos > endBlock.position()) && isEndInMid) cursor.movePosition(QTextCursor::EndOfBlock,QTextCursor::MoveAnchor); cursor.setPosition(currentPos,QTextCursor::KeepAnchor); if (currentPos > startBlock.position()) cursor.movePosition(QTextCursor::StartOfBlock,QTextCursor::KeepAnchor); } // Insert text and restore selection int anchorBefore = cursor.anchor(); int posBefore = cursor.position(); cursor.insertText(text); int posAfter = cursor.position(); if (isDownwards) { cursor.setPosition(anchorBefore,QTextCursor::MoveAnchor); cursor.setPosition(isLastBlock ? posAfter : posAfter-1,QTextCursor::KeepAnchor); } else { cursor.setPosition(posAfter,QTextCursor::MoveAnchor); cursor.setPosition(posBefore,QTextCursor::KeepAnchor); } setTextCursor(cursor); } /** * Auto indents the next line based on the current block level. * For regular lines, the previous line's tabs will be used. * For ending blocks, one tab will be removed. */ void ScriptTextEditor::autoIndent() { const QString data = toPlainText(); QTextCursor cursor = textCursor(); const int cursorPosition = cursor.position(); const QTextBlock previousBlock = cursor.block().previous(); const int previousPosition = previousBlock.position(); QString indent; if (previousBlock.isValid()) { RBXTextUserData* pUserData = dynamic_cast(previousBlock.userData()); if (pUserData) { const int foldState = pUserData->getFoldState(); const QString text = previousBlock.text().trimmed() + ' '; const bool isElse = text.startsWith("else ") || text.startsWith("elseif "); int requiredTabs = 0; // if block is not closed/ended yet or special case else if ((foldState < 0) || isElse) { if (isElse) { // start at previous block QTextBlock block = previousBlock.previous(); int foldState = 0; while (block.isValid()) { const RBXTextUserData* userData = dynamic_cast(block.userData()); if (userData) { // add current block's foldState foldState += userData->getFoldState(); // if we have positive fold state if (foldState > 0) { QString line = block.text(); QString trimmed = line.trimmed() + ' '; if (trimmed.startsWith("if ") || (FFlag::ElseAutoIndentFix && trimmed.startsWith("if("))) { while (line[requiredTabs] == '\t') ++requiredTabs; break; } } } block = block.previous(); } } else { requiredTabs = ScriptEditorUtils::noOfStartingTabs( ScriptEditorUtils::findFoldBoundary(previousBlock,true)); } // make indent string while (indent.length() < requiredTabs) indent.append('\t'); // select the text to replace cursor.setPosition(previousPosition,QTextCursor::MoveAnchor); cursor.setPosition(cursorPosition,QTextCursor::KeepAnchor); QString modifiedText = previousBlock.text(); // remove tabs at the beginning, we're going to set them correctly while (modifiedText.startsWith('\t')) modifiedText = modifiedText.remove(0,1); modifiedText.prepend(indent); // add tabs at beginning modifiedText.append('\n'); modifiedText.append(indent); // add tabs at end if (isElse) modifiedText.append('\t'); cursor.insertText(modifiedText); cursor.clearSelection(); } else { const int noOfOpeningTabs = ScriptEditorUtils::noOfStartingTabs( ScriptEditorUtils::findFoldBoundary(previousBlock,true)); while (indent.length() < noOfOpeningTabs) indent.append('\t'); // if we're opening a new block, indent one more if (foldState > 0) { if (getDocFoldState() > 0) { autoCompleteBlock(text, indent); return; } indent.append('\t'); } cursor.insertText(indent); } } } } void ScriptTextEditor::autoCompleteBlock(const QString& text, QString& indent) { QTextCursor cursor = textCursor(); QString foldEnd = QString("end"); QRegExp commentMatchingExp = QRegExp("(--)?\\[=*\\["); if (text.contains(QRegExp("\\brepeat\\b")) && !text.contains(QRegExp("\\buntil\\b"))) foldEnd = QString("until"); else if ((commentMatchingExp.indexIn(text) > -1) && !text.contains(commentMatchingExp.cap().replace('[', ']'))) foldEnd = QString(commentMatchingExp.cap().replace('[', ']')); else if (text.contains("{") && !text.contains("}")) foldEnd = QString("}"); else if (text.contains("(") && !text.contains(")")) foldEnd = QString(")"); else if (text.contains("function") && text.count("(") > text.count(")")) foldEnd = QString("end)"); indent.append(QString("\t\n") + indent + foldEnd); cursor.insertText(indent); cursor.movePosition(cursor.PreviousBlock); cursor.movePosition(cursor.EndOfBlock); setTextCursor(cursor); } void ScriptTextEditor::checkSyntax() { // Hide any visible tooltip if (QToolTip::isVisible()) QToolTip::hideText(); if (m_pCheckSyntaxTimer) m_pCheckSyntaxTimer->start(FInt::StudioSyntaxCheckTimerInterval); else if (m_pCheckSyntaxThread) QTimer::singleShot(0,this,SLOT(onCheckSyntax())); } void ScriptTextEditor::onCheckSyntax() { if (m_pCheckSyntaxThread) m_pCheckSyntaxThread->requestRun(); } void ScriptTextEditor::startFind() { if (QToolTip::isVisible()) QToolTip::hideText(); m_findSelectionFormat.setBackground(QBrush(QtUtilities::toQColor(AuthoringSettings::singleton().editorFindSelectionBackgroundColor))); m_pFindThread->setAutoFindNext(false); clearFoundItems(); QTimer::singleShot(0,this,SLOT(onFind())); } void ScriptTextEditor::onFind() { if (m_pFindThread) m_pFindThread->requestRun(); } /** * Set the current error message. * This method is invoked. * * @param errorLine line in text, 0 based * @param errorMessage error text, must be copy because of multi-threading */ void ScriptTextEditor::setErrorLine( int errorLine, QString errorMessage) { // make sure we do not show error for invalid line if (FFlag::LuaDebugger && errorLine < 0 && m_errorLine < 0) return; QList extraSelections; QString message = errorMessage; int visibleLine = errorLine; QTextBlock errorBlock = document()->findBlockByNumber(visibleLine); if ( errorBlock.isValid() ) { // if block isn't visible, modify error message if ( !errorBlock.isVisible() ) message = "Error in block: " + errorMessage; // search up until we find a block that is actually visible // this will always succeed if we hit the top level while ( !errorBlock.isVisible() || errorBlock.text().length() == 0 ) errorBlock = ScriptEditorUtils::findFoldBoundary(errorBlock.previous(),true); visibleLine = errorBlock.blockNumber(); // squiggly underline the bad line QTextEdit::ExtraSelection selection; selection.format = m_ErrorFormat; selection.cursor = QTextCursor(errorBlock); selection.cursor.clearSelection(); extraSelections.append(selection); } // if the block is invalid, this will clear any old error line setExtraSelections(extraSelections); // if this is a new error, report to status bar if ( visibleLine >= 0 && ( m_visibleErrorLine != visibleLine || m_errorMessage != message) ) { m_pMainWindow->statusBar()->showMessage(message,5000); } m_errorMessage = message; m_errorLine = errorLine; m_visibleErrorLine = visibleLine; m_pMainWindow->goToScriptErrorAction->setEnabled(m_errorLine >= 0); } void ScriptTextEditor::setAnalysisResult(const RBX::ScriptAnalyzer::Result& analysisResults) { m_scriptMessageForRange.clear(); m_errorLine = -1; removeFromExtraSelections(m_AnalysisSelections); QList extraSelections; RBX::ScriptAnalyzer::Location loc; int startPos = -1, endPos = -1; bool atEof = false; if (analysisResults.error) { RBX::ScriptAnalyzer::Error error = analysisResults.error.get(); loc = error.location; populateValuesFromLocation(loc, startPos, endPos, atEof); if (appendExtraSelection(m_ErrorFormat, startPos, endPos, atEof, m_AnalysisSelections)) { // if this is a new error, report to status bar QString message = error.text.c_str(); m_pMainWindow->statusBar()->showMessage(message, 5000); // save error information (if we need to show tooltip for the entire error line, then set startPos and endPos as 0) ScriptMessage sm = { 0, message, startPos, endPos }; m_scriptMessageForRange.insert(std::make_pair(std::make_pair(startPos, endPos), sm)); m_errorLine = loc.begin.line; } } else { m_analysisResult = analysisResults.intellesenseAnalysis; } for (size_t i = 0; i < analysisResults.warnings.size(); ++i) { RBX::ScriptAnalyzer::Warning warning = analysisResults.warnings[i]; loc = warning.location; populateValuesFromLocation(loc, startPos, endPos, atEof); if (appendExtraSelection(m_WarningFormat, startPos, endPos, atEof, m_AnalysisSelections)) { QString message; message.sprintf("W%03d: %s", warning.code, warning.text.c_str()); ScriptMessage sm = {warning.code, message, startPos, endPos}; m_scriptMessageForRange.insert(std::make_pair(std::make_pair(startPos, endPos), sm)); } } // update editor to refresh errors/warnings status update(); // update goToError action state m_pMainWindow->goToScriptErrorAction->setEnabled(m_errorLine >= 0); // update analysis widget ScriptAnalysisWidget& scriptAnalysis = UpdateUIManager::Instance().getViewWidget(eDW_SCRIPT_ANALYSIS); scriptAnalysis.updateResults(m_pScriptDoc->getCurrentScript(), analysisResults); } bool ScriptTextEditor::appendExtraSelection(const QTextCharFormat& format, int startPos, int endPos, bool atEof, QList& appendList) { if (!atEof && ((startPos < 0) || (endPos < 0) || (endPos < startPos))) return false; QTextEdit::ExtraSelection selection; selection.format = format; selection.cursor = textCursor(); selection.cursor.clearSelection(); if (atEof) { selection.cursor.setPosition(startPos, QTextCursor::MoveAnchor); selection.cursor.movePosition(QTextCursor::StartOfBlock, QTextCursor::MoveAnchor); selection.cursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor); } else { selection.cursor.setPosition(startPos, QTextCursor::MoveAnchor); selection.cursor.setPosition(endPos, QTextCursor::KeepAnchor); } addToExtraSelections(selection); appendList.append(selection); return true; } void ScriptTextEditor::processTooltip(QHelpEvent* evt) { if (!evt) return; // Assumption: showToopTip signal being used only from DebuggerClient so it should be safe to use return value of the signal // we can have issues with this approach, if the same signal is connected to multiple slots. bool handled = false; if (FFlag::LuaDebugger) handled = Q_EMIT showToolTip(evt->globalPos(), getVariableAtPos(evt->globalPos())); if (handled || m_scriptMessageForRange.empty()) { QToolTip::hideText(); evt->ignore(); return; } QPoint pos = viewport()->mapFromGlobal(evt->globalPos()); QTextCursor cursor = cursorForPosition(pos); int cursorPos = cursor.position(); // multimap is required if we've overlaping range! std::pair ret = m_scriptMessageForRange.equal_range(Range(cursorPos, cursorPos)); if (ret.first != ret.second) { for (MessageCollection::const_iterator iter=ret.first; iter != ret.second; ++iter) { if ((cursorPos >= iter->first.first) && (cursorPos <= iter->first.second)) { QToolTip::showText(evt->globalPos(), iter->second.text); evt->accept(); handled = true; break; } } } if (!handled) { QToolTip::hideText(); evt->ignore(); } } void ScriptTextEditor::commentSelection() { bool isBlockUnfolded = false; updateSelection(&isBlockUnfolded); QTextCursor cursor = textCursor(); if ( cursor.isNull() ) return; const int startPos = cursor.selectionStart(); const int endPos = cursor.selectionEnd(); const QTextBlock startBlock = document()->findBlock(startPos); QTextBlock endBlock = document()->findBlock(endPos); const int anchorPos = cursor.anchor(); const int currentPos = cursor.position(); const bool isDownwards = (anchorPos <= currentPos); bool isEndInMid = (endPos > endBlock.position()); if ( !isEndInMid ) endBlock = endBlock.previous(); QString modifiedText; QTextBlock tmpBlock = startBlock; while ( tmpBlock.isValid() && tmpBlock.blockNumber() <= endBlock.blockNumber() ) { // check if we need to unfold the block RBXTextUserData* userData = dynamic_cast(tmpBlock.userData()); if ( userData && userData->getFoldState() && m_pSideWidget->isFolded(tmpBlock) ) { const int lineNumber = tmpBlock.blockNumber() + 1; m_pSideWidget->unfold(lineNumber, false); isBlockUnfolded = true; } modifiedText.append("--"); modifiedText.append(tmpBlock.text()); modifiedText.append("\n"); bool isLastBlock = (tmpBlock == document()->lastBlock()); if ( isLastBlock || (!isDownwards && isEndInMid && (tmpBlock.blockNumber() == endBlock.blockNumber()))) { modifiedText.remove(modifiedText.length() - 1,1); // Remove the extra "\n" } if ( isLastBlock ) break; tmpBlock = tmpBlock.next(); } replaceSelection(startPos,endPos,modifiedText); // now update view updateViewAndCursor(isBlockUnfolded); } void ScriptTextEditor::uncommentSelection() { QTextCursor cursor = textCursor(); if ( cursor.isNull() ) return; const int startPos = cursor.selectionStart(); const int endPos = cursor.selectionEnd(); const QTextBlock startBlock = document()->findBlock(startPos); QTextBlock endBlock = document()->findBlock(endPos); const int anchorPos = cursor.anchor(); const int currentPos = cursor.position(); const bool isDownwards = (anchorPos <= currentPos); bool isEndInMid = (endPos > endBlock.position()); if (!isEndInMid) endBlock = endBlock.previous(); bool isBlockUnfolded = false; QString modifiedText; QTextBlock tmpBlock = startBlock; while (tmpBlock.isValid() && tmpBlock.blockNumber() <= endBlock.blockNumber()) { RBXTextUserData* userData = dynamic_cast(tmpBlock.userData()); if ( userData && userData->getFoldState() && m_pSideWidget->isFolded(tmpBlock) ) { const int lineNumber = tmpBlock.blockNumber() + 1; m_pSideWidget->unfold(lineNumber, false); isBlockUnfolded = true; } QString text = tmpBlock.text(); if (text.simplified().startsWith("--")) text.remove(text.indexOf("--"),2); modifiedText.append(text); modifiedText.append("\n"); bool isLastBlock = (tmpBlock == document()->lastBlock()); if (isLastBlock || (!isDownwards && isEndInMid && (tmpBlock.blockNumber() == endBlock.blockNumber()))) { modifiedText.remove(modifiedText.length() - 1,1); // Remove the extra "\n" } if (isLastBlock) break; tmpBlock = tmpBlock.next(); } replaceSelection(startPos,endPos,modifiedText); // now update view updateViewAndCursor(isBlockUnfolded); } void ScriptTextEditor::toggleCommentSelection() { QTextCursor cursor = textCursor(); if ( cursor.isNull() ) return; const int startPos = cursor.selectionStart(); const int endPos = cursor.selectionEnd(); const QTextBlock startBlock = document()->findBlock(startPos); QTextBlock endBlock = document()->findBlock(endPos); bool commented = true; bool isEndInMid = (endPos > endBlock.position()); if (!isEndInMid) endBlock = endBlock.previous(); QString modifiedText; QTextBlock tmpBlock = startBlock; while (tmpBlock.isValid() && tmpBlock.blockNumber() <= endBlock.blockNumber()) { QString text = tmpBlock.text(); if (!text.startsWith("--")) { commented = false; break; } bool isLastBlock = (tmpBlock == document()->lastBlock()); if (isLastBlock) break; tmpBlock = tmpBlock.next(); } if (commented) uncommentSelection(); else commentSelection(); } void ScriptTextEditor::updateSelection(bool* isBlockUnfolded) { QTextCursor cursor = textCursor(); if ( cursor.isNull() ) return; int startPos = cursor.selectionStart(); int endPos = cursor.selectionEnd(); QTextBlock startBlock = document()->findBlock(startPos); QTextBlock endBlock = document()->findBlock(endPos); RBXTextUserData* userData = dynamic_cast(startBlock.userData()); if ( userData && userData->getFoldState() ) { if ( m_pSideWidget->isFolded(startBlock) ) { const int lineNumber = startBlock.blockNumber() + 1; m_pSideWidget->unfold(lineNumber, false); if (isBlockUnfolded) *isBlockUnfolded = true; } endBlock = ScriptEditorUtils::findFoldBoundary(endBlock,false); if ( endBlock == document()->lastBlock() ) endPos = endBlock.position(); else endPos = endBlock.next().position() - 1; const bool isDownwards = (cursor.anchor() <= cursor.position()); if ( isDownwards ) { cursor.setPosition(startPos,QTextCursor::MoveAnchor); cursor.movePosition(QTextCursor::StartOfBlock,QTextCursor::MoveAnchor); cursor.setPosition(endPos,QTextCursor::KeepAnchor); cursor.movePosition(QTextCursor::EndOfBlock,QTextCursor::KeepAnchor); } else { cursor.setPosition(endPos,QTextCursor::MoveAnchor); cursor.movePosition(QTextCursor::EndOfBlock,QTextCursor::MoveAnchor); cursor.setPosition(startPos,QTextCursor::KeepAnchor); cursor.movePosition(QTextCursor::StartOfBlock,QTextCursor::KeepAnchor); } setTextCursor(cursor); } } /** * Goes to the last error line and ensures visible. */ void ScriptTextEditor::goToError() { if ( m_errorLine < 0 ) return; QTextCursor cursor = textCursor(); QTextBlock errorBlock = document()->findBlockByNumber(m_errorLine); cursor.setPosition(errorBlock.position()); setTextCursor(cursor); ensureVisible(errorBlock); } /** * Ensures the given block is visible by unfolding parents. */ void ScriptTextEditor::ensureVisible(const QTextBlock& block) { QTextBlock current = block; while ( current.isValid() && !current.isVisible() ) { QTextBlock parent = ScriptEditorUtils::findFoldBoundary(current.previous(),true); m_pSideWidget->unfold(parent.blockNumber() + 1); current = parent; } ensureCursorVisible(); updateFolds(); } /** * When updating due to fold changes we need to check syntax again. */ void ScriptTextEditor::updateFolds() { m_pCheckSyntaxThread->requestRun(); update(); } void ScriptTextEditor::expandAllFolds() { expandAllFolds(true); } void ScriptTextEditor::collapseAllFolds() { expandAllFolds(false); } void ScriptTextEditor::goToLine() { FindReplaceProvider::instance().getGotoLineDialog()->activate(textCursor().blockNumber() + 1, document()->blockCount()); } void ScriptTextEditor::find() { if (FFlag::StudioEmbeddedFindDialogEnabled) { FindReplaceProvider::instance().getEmbeddedFindDialog()->activate(); } else { FindReplaceProvider::instance().getReplaceDialog()->hide(); FindReplaceProvider::instance().getFindDialog()->show(); FindReplaceProvider::instance().getFindDialog()->activateWindow(); FindReplaceProvider::instance().getFindDialog()->setFocus(); } } void ScriptTextEditor::moveNext(bool forward) { QMutexLocker lock(&m_selectionMutex); QTextCursor cursor = textCursor(); if (m_foundItems.size() > 0) { std::map::iterator locationIter = forward ? m_foundItems.lower_bound(cursor.selectionEnd()) : m_foundItems.lower_bound(cursor.selectionStart()); if (forward) { if (locationIter != m_foundItems.end()) { cursor.setPosition(locationIter->first, QTextCursor::MoveAnchor); cursor.setPosition(locationIter->second, QTextCursor::KeepAnchor); } else { cursor.setPosition(m_foundItems.begin()->first, QTextCursor::MoveAnchor); cursor.setPosition(m_foundItems.begin()->second, QTextCursor::KeepAnchor); } } else { std::map::reverse_iterator reverseLocationIter(locationIter); if (reverseLocationIter != m_foundItems.rend()) { cursor.setPosition(reverseLocationIter->first, QTextCursor::MoveAnchor); cursor.setPosition(reverseLocationIter->second, QTextCursor::KeepAnchor); } else { cursor.setPosition(m_foundItems.rbegin()->first, QTextCursor::MoveAnchor); cursor.setPosition(m_foundItems.rbegin()->second, QTextCursor::KeepAnchor); } } this->setTextCursor(cursor); } } void ScriptTextEditor::replaceNext(const QRegExp& rx, const QString& text) { QMutexLocker lock(&m_selectionMutex); if (m_foundItems.size() == 0) return; QTextCursor cursor = textCursor(); int startPos = cursor.selectionStart(); std::map::iterator locationIter = m_foundItems.lower_bound(startPos); if (locationIter == m_foundItems.end()) locationIter = m_foundItems.begin(); cursor.beginEditBlock(); cursor.setPosition(locationIter->first, QTextCursor::MoveAnchor); cursor.setPosition(locationIter->second, QTextCursor::KeepAnchor); QString updatedString = cursor.selectedText().replace(rx, text); cursor.removeSelectedText(); cursor.insertText(updatedString); cursor.endEditBlock(); moveNext(true); } void ScriptTextEditor::replaceAll(const QRegExp& rx, const QString& text) { QMutexLocker lock(&m_selectionMutex); if (m_foundItems.size() == 0) return; QTextCursor cursor = textCursor(); cursor.beginEditBlock(); blockSignals(true); for (std::map::reverse_iterator iter = m_foundItems.rbegin(); iter != m_foundItems.rend(); ++iter) { cursor.setPosition(iter->first, QTextCursor::MoveAnchor); cursor.setPosition(iter->second, QTextCursor::KeepAnchor); QString updatedString = cursor.selectedText().replace(rx, text); cursor.removeSelectedText(); cursor.insertText(updatedString); } blockSignals(false); cursor.endEditBlock(); startFind(); } void ScriptTextEditor::findNext(bool moveCursorToBeginningOfSelection) { if (FFlag::StudioEmbeddedFindDialogEnabled) { if (moveCursorToBeginningOfSelection) { QTextCursor cursor = textCursor(); cursor.setPosition(cursor.selectionStart()); setTextCursor(cursor); } moveNext(true); } else { FindReplaceProvider::instance().find(true); if (!FindReplaceProvider::instance().getReplaceDialog()->isVisible() && !FindReplaceProvider::instance().getFindDialog()->isVisible() && !isActiveWindow() ) { activateWindow(); setFocus(); } } } void ScriptTextEditor::findPrevious() { if (FFlag::StudioEmbeddedFindDialogEnabled) { moveNext(false); } else { FindReplaceProvider::instance().find(false); if ( !FindReplaceProvider::instance().getReplaceDialog()->isVisible() && !FindReplaceProvider::instance().getFindDialog()->isVisible() && !isActiveWindow() ) { activateWindow(); setFocus(); } } } void ScriptTextEditor::replace() { if (FFlag::StudioEmbeddedFindDialogEnabled) { FindReplaceProvider::instance().getEmbeddedFindDialog()->activate(true); } else { FindReplaceProvider::instance().getFindDialog()->hide(); FindReplaceProvider::instance().getReplaceDialog()->show(); FindReplaceProvider::instance().getReplaceDialog()->activateWindow(); FindReplaceProvider::instance().getReplaceDialog()->setFocus(); } } bool ScriptTextEditor::doHandleAction(const QString& actionID) { bool handled = true; QString ignoredActions = "undoAction redoAction copyAction cutAction pasteAction "; if (!FFlag::StudioSeparateActionByActivationMethod && ignoredActions.contains(actionID) ) return handled; if (actionID == "undoAction") undo(); else if (actionID == "redoAction") redo(); else if (actionID == "copyAction") copy(); else if (actionID == "cutAction") cut(); else if (actionID == "pasteAction") paste(); else if ( actionID == "findAction" ) find(); else if ( actionID == "replaceAction" ) replace(); else if ( actionID == "findNextAction" ) findNext(); else if ( actionID == "findPreviousAction" ) findPrevious(); else if ( actionID == "goToScriptErrorAction" ) goToError(); else if ( actionID == "goToLineAction" ) goToLine(); else if ( actionID == "commentSelectionAction" ) commentSelection(); else if ( actionID == "uncommentSelectionAction" ) uncommentSelection(); else if ( actionID == "toggleCommentAction" ) toggleCommentSelection(); else if ( actionID == "expandAllFoldsAction" ) expandAllFolds(); else if ( actionID == "collapseAllFoldsAction" ) collapseAllFolds(); else if ( actionID == "deleteSelectedAction" ) textCursor().removeSelectedText(); else if ( actionID == "selectAllAction" ) selectAll(); else if ( actionID == "zoomInAction" ) zoom(1); else if ( actionID == "zoomOutAction" ) zoom(-1); else if ( actionID == "resetScriptZoomAction" ) #ifdef Q_WS_MAC zoom(14, true); #else zoom(10, true); #endif else handled = false; return handled; } bool ScriptTextEditor::isPasteEnabled() { QClipboard *pClipboard = QApplication::clipboard(); if (pClipboard) { const QMimeData *pMimeData = pClipboard->mimeData(); if (pMimeData && pMimeData->hasFormat("text/plain")) return true; } return false; } bool ScriptTextEditor::actionState(const QString& actionID,bool& enableState,bool& checkedState) { if (isReadOnly()) { // disable actions which can edit script code static QString editableActionList = "undoAction redoAction pasteAction cutAction copyAction commentSelectionAction uncommentSelectionAction " "deleteSelectedAction selectAllAction toggleCommentAction " "simulationRunAction simulationPlayAction simulationStopAction simulationResetAction "; if (editableActionList.contains(actionID)) { enableState = false; checkedState = false; return true; } } static QString disableActionList = "groupSelectionAction ungroupSelectionAction pasteIntoAction"; if (disableActionList.contains(actionID)) { enableState = false; checkedState = false; return true; } if ( actionID == "findNextAction" || actionID == "findPreviousAction" ) enableState = FFlag::StudioEmbeddedFindDialogEnabled || FindReplaceProvider::instance().isFindNextEnabled(); else if ( actionID == "goToScriptErrorAction" ) enableState = m_errorLine >= 0; else if ( actionID == "undoAction" ) enableState = document()->isUndoAvailable(); else if ( actionID == "redoAction" ) enableState = document()->isRedoAvailable(); else if ( actionID == "pasteAction" ) enableState = isPasteEnabled(); else if ( actionID == "selectAllAction" ) enableState = true; else if ( actionID == "zoomInAction" ) enableState = AuthoringSettings::singleton().editorFont.pointSize() < MaxFontZoomIn; else if ( actionID == "zoomOutAction" ) enableState = AuthoringSettings::singleton().editorFont.pointSize() > MaxFontZoomOut; else { static QString requireSelectionActions = "cutAction copyAction commentSelectionAction uncommentSelectionAction deleteSelectedAction"; if ( requireSelectionActions.contains(actionID) ) enableState = textCursor().hasSelection(); else return false; } return true; } void ScriptTextEditor::onPropertyChanged(const RBX::Reflection::PropertyDescriptor* pDescriptor) { bool updateText = false; // if there is a color change, update the syntax colors if ( !pDescriptor || pDescriptor->category.str == sScriptColorsCategoryName ) { updateText = true; // update viewport palette before setting stylesheet or else cursor color will not be changed QPalette pt = viewport()->palette(); pt.setColor(QPalette::Text, QtUtilities::toQColor(AuthoringSettings::singleton().editorTextColor)); viewport()->setPalette(pt); m_pSyntaxHighlighter->onPropertyChanged(pDescriptor); setStyleSheet(constructStyleSheet()); // update error format background color QColor errorColor = QtUtilities::toQColor(AuthoringSettings::singleton().editorErrorColor); QColor warningColor = QtUtilities::toQColor(AuthoringSettings::singleton().editorWarningColor); // first update selection QList currentSelections = extraSelections(); for (int ii = 0; ii < currentSelections.size(); ++ii) { if (currentSelections[ii].format == m_ErrorFormat) { currentSelections[ii].format.setUnderlineColor(errorColor); } else if (currentSelections[ii].format == m_WarningFormat) { currentSelections[ii].format.setUnderlineColor(warningColor); } } setExtraSelections(currentSelections); // now update member variables m_ErrorFormat.setUnderlineColor(errorColor); m_WarningFormat.setUnderlineColor(warningColor); } if ( !pDescriptor || pDescriptor->category.str == sScriptCategoryName ) { m_pSideWidget->onPropertyChanged(pDescriptor); updateText = true; // set the new font, make sure it's fixed pitch QFont font = AuthoringSettings::singleton().editorFont; font.setFixedPitch(true); font.setUnderline(false); font.setStrikeOut(false); font.setBold(false); setFont(font); m_pSideWidget->onPropertyChanged(pDescriptor); // adjust size of tabs based on font width and size of tab stop const QFontMetrics metrics(font); const int spaceWidth = metrics.width(' '); setTabStopWidth(AuthoringSettings::singleton().editorTabWidth * spaceWidth); if ( AuthoringSettings::singleton().editorTextWrap ) setLineWrapMode(QPlainTextEdit::WidgetWidth); else setLineWrapMode(QPlainTextEdit::NoWrap); // adjust error text color m_ErrorFormat.setFont(font); m_ErrorFormat.setProperty(QTextFormat::FullWidthSelection,true); m_ErrorFormat.setFontUnderline(true); m_ErrorFormat.setUnderlineStyle(QTextCharFormat::SpellCheckUnderline); // adjust warning text m_WarningFormat.setFont(font); m_WarningFormat.setProperty(QTextFormat::FullWidthSelection,true); m_WarningFormat.setFontUnderline(true); m_WarningFormat.setUnderlineStyle(QTextCharFormat::SpellCheckUnderline); } // clear and reset the text to change colors and formating if ( updateText ) { m_pSyntaxHighlighter->setFont(font()); m_pSyntaxHighlighter->rehighlight(); } } /** * Checks if a character is part of a valid word. * Characters that are not alpha numeric or an underscore break up words. * * @param c character to check * @return true if the character is part of a word */ bool ScriptTextEditor::isWordCharacter(const QChar& c) { if ( c == '_' || c.isLetterOrNumber() ) return true; else return false; } /** * Finds a regular expression in a document text block. * * @param block block to search * @param expression regular expression to search for * @param offset offset in block * @param option search options * @param cursor output cursor * @return true if found */ bool ScriptTextEditor::findInBlock( const QTextBlock& block, const QRegExp& expression, int offset, QTextDocument::FindFlags options, QTextCursor& cursor ) { const QRegExp expr(expression); QString text = block.text(); text.replace(QChar::Nbsp,QLatin1Char(' ')); int idx = -1; while ( offset >=0 && offset <= text.length() ) { // move to next character index idx = (options & QTextDocument::FindBackward) ? expr.lastIndexIn(text,offset) : expr.indexIn(text,offset); if ( idx == -1 ) return false; if ( options & QTextDocument::FindWholeWords ) { // check for word breaks const int start = idx; const int end = start + expr.matchedLength(); if ( (start != 0 && isWordCharacter(text.at(start - 1))) || (end != text.length() && isWordCharacter(text.at(end))) ) { //if this is not a whole word, continue the search in the string offset = (options & QTextDocument::FindBackward) ? idx - 1 : end + 1; idx = -1; continue; } } //we have a hit, return the cursor for that. break; } if ( idx == -1 ) return false; // generate output cursor cursor = QTextCursor(block.docHandle(),block.position() + idx); cursor.setPosition(cursor.position() + expr.matchedLength(),QTextCursor::KeepAnchor); return true; } /** * Finds a regular expression in a text document. * * @param doc document to search * @param expr regular expression * @param from starting cursor location * @param option search options * @return cursor if found */ QTextCursor ScriptTextEditor::findInDoc( const QTextDocument& doc, const QRegExp& expr, const QTextCursor& from, QTextDocument::FindFlags options ) { if ( expr.isEmpty() ) return QTextCursor(); int pos = 0; if ( !from.isNull() ) { // skip over current selection if ( options & QTextDocument::FindBackward ) pos = from.selectionStart(); else pos = from.selectionEnd(); } // the cursor is positioned between characters, so for a backward search // do not include the character given in the position. if ( options & QTextDocument::FindBackward ) { --pos ; if ( pos < 0 ) return QTextCursor(); } QTextCursor cursor; QTextBlock block = doc.findBlock(pos); if ( options & QTextDocument::FindBackward ) { // search backwards int blockOffset = pos - block.position(); while ( block.isValid() ) { if ( findInBlock(block,expr,blockOffset,options,cursor) ) return cursor; block = block.previous(); blockOffset = block.length() - 1; } } else { // search forwards int blockOffset = qMax(0, pos - block.position()); while ( block.isValid() ) { if ( findInBlock(block,expr,blockOffset,options,cursor) ) return cursor; blockOffset = 0; block = block.next(); } } return QTextCursor(); } bool ScriptTextEditor::find(bool loop,const QRegExp& regExp,QTextDocument::FindFlags flags) { if (FFlag::StudioEmbeddedFindDialogEnabled) { clearFoundItems(); m_pFindThread->setAutoFindNext(true); m_pFindThread->setData(regExp, flags); m_pFindThread->requestRun(); return false; } else { QTextCursor cursor = findInDoc(*document(),regExp,textCursor(),flags); if ( cursor.isNull() && loop ) { cursor = textCursor(); if ( flags & QTextDocument::FindBackward ) { UpdateUIManager::Instance().getMainWindow().statusBar()->showMessage("Search wrapped to end",2000); cursor.movePosition(QTextCursor::End); } else { UpdateUIManager::Instance().getMainWindow().statusBar()->showMessage("Search wrapped to start",2000); cursor.movePosition(QTextCursor::Start); } cursor = findInDoc(*document(),regExp,cursor,flags); } if ( !cursor.isNull() ) { setTextCursor(cursor); ensureVisible(cursor.block()); } return !cursor.isNull(); } } void ScriptTextEditor::replace(const QString& text,const QRegExp& regExp,QTextDocument::FindFlags flags) { textCursor().insertText(text); } QString ScriptTextEditor::getSelectedText() { return textCursor().selectedText(); } bool ScriptTextEditor::hasSelection() { return textCursor().hasSelection(); } void ScriptTextEditor::goToStart(QTextDocument::FindFlags flags) { QTextCursor cursor = textCursor(); // The start of the search will be the end of the document when we // are searching backwards. if ( flags & QTextDocument::FindBackward ) { cursor.movePosition(QTextCursor::End); } else { cursor.movePosition(QTextCursor::Start); } setTextCursor(cursor); } int ScriptTextEditor::getDocFoldState(int blockNumber) { int foldCount = 0; QTextBlock iterBlock = document()->findBlockByLineNumber(0); if (!blockNumber) blockNumber = document()->blockCount(); while (iterBlock.isValid() && iterBlock.blockNumber() <= blockNumber) { RBXTextUserData* pUserData = dynamic_cast(iterBlock.userData()); if (pUserData) foldCount += pUserData->getFoldState(); iterBlock = iterBlock.next(); } return foldCount; } QString ScriptTextEditor::constructStyleSheet() { QString styleSheet; // set style sheet so we can change the text color, background color, // selected text color, and selected background color styleSheet = QString(STRINGIFY( QPlainTextEdit { color: %1; background: %2; selection-color: %3; selection-background-color: %4; } )). arg(QtUtilities::toQColor(AuthoringSettings::singleton().editorTextColor).name()). arg(QtUtilities::toQColor(AuthoringSettings::singleton().editorBackgroundColor).name()). arg(QtUtilities::toQColor(AuthoringSettings::singleton().editorSelectionColor).name()). arg(QtUtilities::toQColor(AuthoringSettings::singleton().editorSelectionBackgroundColor).name()); return styleSheet; } QString ScriptTextEditor::getVariableAtPos(const QPoint& globalPos) { QPoint pos = viewport()->mapFromGlobal(globalPos); QTextCursor cursor = cursorForPosition(pos); if (cursor.block().isValid()) { // get the cursor position before selecting the block text int cursorPos = cursor.position() - cursor.block().position(); // select the entire block cursor.select(QTextCursor::BlockUnderCursor); // get block rect QRect blockRect = cursorRect(cursor); blockRect.setLeft(0); // if mouse position is inside the block rect then only proceed if (blockRect.contains(pos)) { // get block text under the cursor QString blockText = cursor.selectedText(); if (cursorPos > 0 && cursorPos < blockText.size() && blockText.at(cursorPos).isLetterOrNumber()) { int startPos = variableStartPos(blockText, cursorPos); int endPos = variableEndPos(blockText, cursorPos); if (endPos > startPos) return blockText.mid(startPos, endPos - startPos); } } } return QString(); } int ScriptTextEditor::variableStartPos(const QString &text, int startPos) { // variable in Lua can have letter, number, underscore and member variables can be accessed via dot for ( ; startPos >= 0; startPos--) { int tempPos = startPos; if (startPos > 0 && text.at(startPos) == '.') tempPos = startPos-1; if (!isWordCharacter(text.at(tempPos))) return startPos+1; } return 0; } int ScriptTextEditor::variableEndPos(const QString &text, int endPos) { for ( ; endPos < text.size(); endPos++) { if (!isWordCharacter(text.at(endPos))) return endPos; } return text.size(); } void ScriptTextEditor::moveCursorToLine(int line) { QTextCursor cursor = textCursor(); if (document()->blockCount() < line) return; int position = document()->findBlockByNumber(line).position(); cursor.setPosition(position); setTextCursor(cursor); ensureVisible(cursor.block()); } void ScriptTextEditor::selectText(int startPosition, int endPosition) { QTextCursor cursor = textCursor(); cursor.setPosition(startPosition); cursor.setPosition(endPosition, QTextCursor::KeepAnchor); setTextCursor(cursor); ensureVisible(cursor.block()); } void ScriptTextEditor::selectText(const RBX::ScriptAnalyzer::Location& location, bool trySettingBlocksVisible) { int startPos = getStartPosition(location); int endPos = getEndPosition(location); if (startPos >= 0 && endPos >= 0 && (endPos >= startPos)) { selectText(startPos, endPos); } else if (trySettingBlocksVisible) { // if we couldn't get valid start and end pos, then try to unfold blocks QTextBlock startBlock = document()->findBlockByNumber(location.begin.line); if (startBlock.isValid() && !startBlock.isVisible()) ensureVisible(startBlock); QTextBlock endBlock = document()->findBlockByNumber(location.end.line); if (endBlock.isValid() && !endBlock.isVisible()) ensureVisible(endBlock); // again try to select text selectText(location, false); } centerCursor(); } bool ScriptTextEditor::addToExtraSelections(const QTextEdit::ExtraSelection& extraSelection, bool prepend) { QList currentSelections = extraSelections(); if (currentSelections.contains(extraSelection)) return false; prepend ? currentSelections.push_front(extraSelection) : currentSelections.append(extraSelection); setExtraSelections(currentSelections); return true; } void ScriptTextEditor::removeFromExtraSelections(QList selectionsToRemove) { QList currentSelections = extraSelections(); for (int ii = 0; ii < selectionsToRemove.size(); ++ii) currentSelections.removeOne(selectionsToRemove.at(ii)); setExtraSelections(currentSelections); } void ScriptTextEditor::updateViewAndCursor(bool resizeRequired) { // if block was unfolded, then we will need to resize the viewport if ( resizeRequired ) { viewport()->resize(viewport()->sizeHint()); // viewport gets updated only after a draw call so delay cursor visibility // if we don't, then incorrect scrolling will happen QTimer::singleShot(0,this,SLOT(updateCursorVisibility())); } else { // directly update cursor updateCursorVisibility(); } // initiate syntax check updateFolds(); } void ScriptTextEditor::updateCursorVisibility() { ensureCursorVisible(); } void ScriptTextEditor::updateWikiLookup(const char* key, std::string value) { std::pair lookupPair = wikiLookup[key]; lookupPair.first++; if(lookupPair.first > 1) lookupPair.second = ""; else lookupPair.second = value; wikiLookup[key] = lookupPair; } void ScriptTextEditor::setupWikiLookup() { for(RBX::Reflection::ClassDescriptor::ClassDescriptors::const_iterator iter = RBX::Reflection::ClassDescriptor::all_begin(); iter!=RBX::Reflection::ClassDescriptor::all_end();++iter) { std::string className = ((*iter)->name).toString(); updateWikiLookup((*iter)->name.c_str(), "API:Class/" + className); for(RBX::Reflection::MemberDescriptorContainer::Collection::const_iterator propIter = (*iter)->begin(); propIter!=(*iter)->end();++propIter) if(className.compare((*propIter)->owner.name.toString()) == 0) updateWikiLookup((*propIter)->name.c_str(), ("API:Class/" + className + "/" + (*propIter)->name.toString()).c_str()); for(RBX::Reflection::MemberDescriptorContainer::Collection::const_iterator funcIter = (*iter)->begin(); funcIter!=(*iter)->end();++funcIter) if(className.compare((*funcIter)->owner.name.toString()) == 0) updateWikiLookup((*funcIter)->name.c_str(), ("API:Class/" + className + "/" + (*funcIter)->name.toString()).c_str()); for(RBX::Reflection::MemberDescriptorContainer::Collection::const_iterator yfuncIter = (*iter)->begin(); yfuncIter!=(*iter)->end();++yfuncIter) if(className.compare((*yfuncIter)->owner.name.toString()) == 0) updateWikiLookup((*yfuncIter)->name.c_str(), ("API:Class/" + className + "/" + (*yfuncIter)->name.toString()).c_str()); for(RBX::Reflection::MemberDescriptorContainer::Collection::const_iterator eventIter = (*iter)->begin(); eventIter!=(*iter)->end();++eventIter) if(className.compare((*eventIter)->owner.name.toString()) == 0) updateWikiLookup((*eventIter)->name.c_str(), ("API:Class/" + className + "/" + (*eventIter)->name.toString()).c_str()); for(RBX::Reflection::MemberDescriptorContainer::Collection::const_iterator callbackIter = (*iter)->begin(); callbackIter!=(*iter)->end();++callbackIter) if(className.compare((*callbackIter)->owner.name.toString()) == 0) updateWikiLookup((*callbackIter)->name.c_str(), ("API:Class/" + className + "/" + (*callbackIter)->name.toString()).c_str()); } for(std::vector< const RBX::Reflection::EnumDescriptor* >::const_iterator enumIter = RBX::Reflection::EnumDescriptor::enumsBegin(); enumIter!=RBX::Reflection::EnumDescriptor::enumsEnd();++enumIter) { std::string enumName = (*enumIter)->name.toString(); updateWikiLookup((*enumIter)->name.c_str(), "API:Enum/" + enumName); for(std::vector< const RBX::Reflection::EnumDescriptor::Item* >::const_iterator itemItor = (*enumIter)->begin(); itemItor!=(*enumIter)->end(); ++itemItor) { std::string itemName = (*itemItor)->name.toString(); updateWikiLookup((*itemItor)->name.c_str(), "API:Enum/" + enumName); } } } void ScriptTextEditor::selectionChangedSearch() { if (textCursor().hasSelection()) { QString selection = textCursor().selectedText(); if(selection.isEmpty()) m_pWikiSearchTimer->stop(); else { if (!m_pWikiSearchTimer->isActive()) { m_pWikiSearchTimer->start(WikiSearchTimerInterval); currentWikiSearchSelection = selection; } else if (QString::compare(selection, currentWikiSearchSelection) != 0) { m_pWikiSearchTimer->start(WikiSearchTimerInterval); currentWikiSearchSelection = selection; } } } } void ScriptTextEditor::onSearchTimeout() { if (!currentWikiSearchSelection.isEmpty()) { QString searchTerm = currentWikiSearchSelection; std::pair lookupPair = wikiLookup[searchTerm.toStdString().c_str()]; if (lookupPair.first == 1) searchTerm = QString(lookupPair.second.c_str()); else { // Don't search for special characters! if (FFlag::ScriptContextSearchFix && searchTerm.contains(QRegExp("[^0-9a-zA-Z]"))) return; searchTerm.prepend("Special:Search?search="); searchTerm.append("&fulltext=Search&studiomode=true"); } Q_EMIT(helpTopicChanged(searchTerm)); } } void ScriptTextEditor::populateValuesFromLocation(const RBX::ScriptAnalyzer::Location& location, int& startPos, int& endPos, bool& atEof) { atEof = (location.begin.line + 1 == (unsigned int)document()->lineCount() && (location.begin.column == location.end.column)); if (!atEof) { startPos = getStartPosition(location); endPos = getEndPosition(location); } else { QTextBlock beginBlock = document()->findBlockByLineNumber(location.begin.line); startPos = beginBlock.position(); endPos = startPos + beginBlock.length(); } } int ScriptTextEditor::getStartPosition(const RBX::ScriptAnalyzer::Location& location) { QTextBlock beginBlock = document()->findBlockByNumber(location.begin.line); if (beginBlock.isValid() && beginBlock.isVisible()) return beginBlock.position() + location.begin.column; return -1; } int ScriptTextEditor::getEndPosition(const RBX::ScriptAnalyzer::Location& location) { QTextBlock endBlock = document()->findBlockByNumber(location.end.line); if (endBlock.isValid() && endBlock.isVisible()) return endBlock.position() + location.end.column; return -1; } void ScriptTextEditor::onCheckSyntaxTimerTimeOut() { m_pCheckSyntaxTimer->stop(); onCheckSyntax(); } void ScriptTextEditor::addFoundItem(int start, int end) { QMutexLocker lock(&m_selectionMutex); m_foundItems.insert(std::pair(start, end)); std::map::iterator firstAfter = m_extraSelectionsToAdd.lower_bound(start); if (m_extraSelectionsToAdd.size() > 0 && firstAfter != m_extraSelectionsToAdd.begin()) { --firstAfter; int previousStart = firstAfter->first; int previousEnd = firstAfter->second; if (start <= previousEnd && end >= previousStart) { start = qMin(start, previousStart); end = qMax(end, previousEnd); m_extraSelectionsToAdd.erase(firstAfter); } } m_extraSelectionsToAdd.insert(std::pair(start, end)); } void ScriptTextEditor::clearFoundItems() { QMutexLocker lock(&m_selectionMutex); m_foundItems.clear(); for (QList::iterator iter = m_currentFoundItems.begin(); iter != m_currentFoundItems.end(); ++iter) m_extraSelectionsToRemove.insert(std::pair(iter->cursor.selectionStart(), *iter)); m_extraSelectionsToAdd.clear(); m_currentFoundItems.clear(); QApplication::postEvent(this, new RobloxCustomEvent(EXTRASELECTION_UPDATE)); } void ScriptTextEditor::updateFindSelection() { QApplication::removePostedEvents(this, EXTRASELECTION_UPDATE); RBX::Time timeCheck = RBX::Time::now(); bool selectionsUpdated = false; QTextEdit::ExtraSelection selection; selection.cursor = textCursor(); int firstVisibleBlockPosition = firstVisibleBlock().position(); QMutexLocker lock(&m_selectionMutex); while ((RBX::Time::now() - timeCheck).msec() < 10) { if (m_extraSelectionsToAdd.empty()) break; std::map::iterator selectionToMake = m_extraSelectionsToAdd.lower_bound(firstVisibleBlockPosition); if (selectionToMake == m_extraSelectionsToAdd.end()) selectionToMake = m_extraSelectionsToAdd.begin(); selection.format = m_findSelectionFormat; selection.cursor.setPosition(selectionToMake->first, QTextCursor::MoveAnchor); selection.cursor.setPosition(selectionToMake->second, QTextCursor::KeepAnchor); m_currentSelections.append(selection); m_currentFoundItems.append(selection); m_extraSelectionsToAdd.erase(selectionToMake); } timeCheck = RBX::Time::now(); while ((RBX::Time::now() - timeCheck).msec() < 10) { for (int i = 0; i < 100; ++i) { if (m_extraSelectionsToRemove.empty()) break; std::multimap::iterator selectionToRemove = m_extraSelectionsToRemove.lower_bound(firstVisibleBlockPosition); if (selectionToRemove == m_extraSelectionsToRemove.end()) selectionToRemove = m_extraSelectionsToRemove.begin(); m_currentSelections.removeOne(selectionToRemove->second); m_extraSelectionsToRemove.erase(selectionToRemove); } setExtraSelections(m_currentSelections); if (m_extraSelectionsToRemove.empty()) break; } if (!m_extraSelectionsToAdd.empty() || !m_extraSelectionsToRemove.empty()) QApplication::postEvent(this, new RobloxCustomEvent(EXTRASELECTION_UPDATE)); } /*****************************************************************************/ // ScriptTraversalThread /*****************************************************************************/ ScriptTraversalThread::ScriptTraversalThread(ScriptTextEditor *parent, RobloxScriptDoc *document) :QThread(parent), m_pScriptEditor(parent), m_pDocument(document), restart(false), abort(false) {} ScriptTraversalThread::~ScriptTraversalThread() { mutex.lock(); abort = true; condition.wakeOne(); mutex.unlock(); wait(); } void ScriptTraversalThread::requestRun() { if (!isRunning()) { start(IdlePriority); } else { restart = true; condition.wakeOne(); } } void ScriptTraversalThread::run() { while (true) { if (abort) return; doWork(); if (!restart) condition.wait(&mutex); restart = false; } } /*****************************************************************************/ // CheckSyntaxThread /*****************************************************************************/ void CheckSyntaxThread::doWork() { shared_ptr dm = m_pDocument->getDataModel(); void* resultPtr = NULL; { RBX::DataModel::LegacyLock l(dm.get(), RBX::DataModelJob::Write); std::string code = m_pScriptEditor->toPlainText().toStdString(); shared_ptr scriptInstance = m_pDocument->getCurrentScript().toInstance(); // Linked script source won't have an instance, so use a fake one to appease the analyzer if (!scriptInstance) { scriptInstance = RBX::Creatable::create(); } RBX::ScriptAnalyzer::Result result = RBX::ScriptAnalyzer::analyze(dm.get(), scriptInstance, code); QMetaObject::invokeMethod(m_pScriptEditor, "setAnalysisResult", Qt::QueuedConnection, Q_ARG(RBX::ScriptAnalyzer::Result, result)); } } /*****************************************************************************/ // FindThread /*****************************************************************************/ void FindThread::setData(const QRegExp& regExp,QTextDocument::FindFlags flags) { restart = true; toSearch = regExp; searchFlags = flags; } bool FindThread::findInBlockNoCursor( const QString& text, int startOfBlock, const QRegExp& expression, int offset, QTextDocument::FindFlags options, int& start, int& end) { const QRegExp expr(expression); int idx = -1; while ( offset >=0 && offset <= text.length() ) { // move to next character index idx = (options & QTextDocument::FindBackward) ? expr.lastIndexIn(text,offset) : expr.indexIn(text,offset); if ( idx == -1 ) return false; if ( options & QTextDocument::FindWholeWords ) { // check for word breaks const int start = idx; const int end = start + expr.matchedLength(); if ( (start != 0 && ScriptTextEditor::isWordCharacter(text.at(start - 1))) || (end != text.length() && ScriptTextEditor::isWordCharacter(text.at(end))) ) { //if this is not a whole word, continue the search in the string offset = end + 1; idx = -1; continue; } } //we have a hit, return the cursor for that. break; } if ( idx == -1 ) return false; // generate output cursor start = startOfBlock + idx; end = start + expr.matchedLength(); return true; } void FindThread::doWork() { m_pScriptEditor->clearFoundItems(); if (toSearch.isEmpty()) { QMetaObject::invokeMethod(FindReplaceProvider::instance().getEmbeddedFindDialog(), "setNumberOfFoundResults", Q_ARG(int, -1)); QMetaObject::invokeMethod(m_pScriptEditor, "onCheckSyntax"); return; } shared_ptr dm = m_pDocument->getDataModel(); RBX::DataModel::LegacyLock lock(dm.get(), RBX::DataModelJob::Write); QTextBlock block; int offset = 0; block = m_pScriptEditor->firstVisibleBlock(); m_searchStartBlockLine = block.blockNumber(); m_searchLastBlockLine = -1; int start, end; QString blockText = block.text(); int blockStart = block.position(); RBX::Time timeCheck = RBX::Time::now(); bool hasFoundItem = false; while (m_searchStartBlockLine != m_searchLastBlockLine) { { QMutexLocker lock(&mutex); if (restart || abort) return; } if (findInBlockNoCursor(blockText, blockStart, toSearch, offset, searchFlags, start, end)) { offset = start - block.position() + 1; m_pScriptEditor->addFoundItem(start, end); hasFoundItem = true; } else { offset = 0; blockStart = blockStart + block.length(); block = block.next(); if (!block.isValid()) { block = m_pScriptEditor->document()->firstBlock(); blockStart = 0; } blockText = block.text(); blockText.replace(QChar::Nbsp,QLatin1Char(' ')); m_searchLastBlockLine = block.blockNumber(); } if (hasFoundItem && (RBX::Time::now() - timeCheck).msec() > 20) { timeCheck = RBX::Time::now(); hasFoundItem = false; QApplication::postEvent(m_pScriptEditor, new RobloxCustomEvent(EXTRASELECTION_UPDATE)); } } if (hasFoundItem) QApplication::postEvent(m_pScriptEditor, new RobloxCustomEvent(EXTRASELECTION_UPDATE)); QMetaObject::invokeMethod(FindReplaceProvider::instance().getEmbeddedFindDialog(), "setNumberOfFoundResults", Q_ARG(int, m_pScriptEditor->numberOfFoundItems())); QMetaObject::invokeMethod(m_pScriptEditor, "onCheckSyntax"); if (FindReplaceProvider::instance().getEmbeddedFindDialog()->hasFocus()) QMetaObject::invokeMethod(m_pScriptEditor, "findNext", Q_ARG(bool, true)); } /*****************************************************************************/ // ScriptEditorUtils /*****************************************************************************/ QTextBlock ScriptEditorUtils::findFoldBoundary( QTextBlock endBlock, bool bUp) { QTextBlock blockFound = endBlock; int nFolds = 0; while (blockFound.isValid()) { RBXTextUserData* pUserData = dynamic_cast(blockFound.userData()); if (pUserData) nFolds += pUserData->getFoldState(); if (bUp) { if (nFolds >= 0) break; blockFound = blockFound.previous(); } else { if (nFolds <= 0) break; blockFound = blockFound.next(); } } return blockFound; } int ScriptEditorUtils::noOfStartingTabs(QTextBlock block) { if (!block.isValid()) return -1; int nTabs = 0; QString text = block.text(); while (text.mid(nTabs,1) == "\t") { nTabs++; } return nTabs; }