diff --git a/kdevplatform/shell/textdocument.cpp b/kdevplatform/shell/textdocument.cpp index 522307e982..6d25662f0f 100644 --- a/kdevplatform/shell/textdocument.cpp +++ b/kdevplatform/shell/textdocument.cpp @@ -1,839 +1,844 @@ /*************************************************************************** * Copyright 2007 Alexander Dymo * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU Library General Public License as * * published by the Free Software Foundation; either version 2 of the * * License, or (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU Library General Public * * License along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * ***************************************************************************/ #include "textdocument.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include +#include + #include "core.h" #include "mainwindow.h" #include "uicontroller.h" #include "partcontroller.h" #include "plugincontroller.h" #include "documentcontroller.h" #include "ktexteditorpluginintegration.h" #include "debug.h" #include #include namespace KDevelop { const int MAX_DOC_SETTINGS = 20; // This sets cursor position and selection on the view to the given // range. Selection is set only for non-empty ranges // Factored into a function since its needed in 3 places already static void selectAndReveal( KTextEditor::View* view, const KTextEditor::Range& range ) { Q_ASSERT(view); if (range.isValid()) { view->setCursorPosition(range.start()); if (!range.isEmpty()) { view->setSelection(range); } } } class TextDocumentPrivate { public: explicit TextDocumentPrivate(TextDocument *textDocument) : q(textDocument) { } ~TextDocumentPrivate() { // Handle the case we are being deleted while the context menu is not yet hidden. // We want to remove all actions we added to it, especially those not owned by the document // but by the plugins (i.e. created on-the-fly during ContextMenuExtension::populateMenu // with ownership set to our addedContextMenu) cleanContextMenu(); saveSessionConfig(); delete document; } void setStatus(KTextEditor::Document* document, bool dirty) { QIcon statusIcon; if (document->isModified()) if (dirty) { state = IDocument::DirtyAndModified; statusIcon = QIcon::fromTheme(QStringLiteral("edit-delete")); } else { state = IDocument::Modified; statusIcon = QIcon::fromTheme(QStringLiteral("document-save")); } else if (dirty) { state = IDocument::Dirty; statusIcon = QIcon::fromTheme(QStringLiteral("document-revert")); } else { state = IDocument::Clean; } q->notifyStateChanged(); Core::self()->uiControllerInternal()->setStatusIcon(q, statusIcon); } inline KConfigGroup katePartSettingsGroup() const { return KSharedConfig::openConfig()->group("KatePart Settings"); } inline QString docConfigGroupName() const { return document->url().toDisplayString(QUrl::PreferLocalFile); } inline KConfigGroup docConfigGroup() const { return katePartSettingsGroup().group(docConfigGroupName()); } void saveSessionConfig() { if(document && document->url().isValid()) { // make sure only MAX_DOC_SETTINGS entries are stored KConfigGroup katePartSettings = katePartSettingsGroup(); // ordered list of documents QStringList documents = katePartSettings.readEntry("documents", QStringList()); // ensure this document is "new", i.e. at the end of the list documents.removeOne(docConfigGroupName()); documents.append(docConfigGroupName()); // remove "old" documents + their group while(documents.size() >= MAX_DOC_SETTINGS) { katePartSettings.group(documents.takeFirst()).deleteGroup(); } // update order katePartSettings.writeEntry("documents", documents); // actually save session config KConfigGroup group = docConfigGroup(); document->writeSessionConfig(group); } } void loadSessionConfig() { if (!document || !katePartSettingsGroup().hasGroup(docConfigGroupName())) { return; } document->readSessionConfig(docConfigGroup(), {QStringLiteral("SkipUrl")}); } // Determines whether the current contents of this document in the editor // could be retrieved from the VCS if they were dismissed. void queryCanRecreateFromVcs(KTextEditor::Document* document) const { // Find projects by checking which one contains the file's parent directory, // to avoid issues with the cmake manager temporarily removing files from a project // during reloading. KDevelop::Path path(document->url()); const auto projects = Core::self()->projectController()->projects(); auto projectIt = std::find_if(projects.begin(), projects.end(), [&](KDevelop::IProject* project) { return project->path().isParentOf(path); }); if (projectIt == projects.end()) { return; } IProject* project = *projectIt; IContentAwareVersionControl* iface; iface = qobject_cast< KDevelop::IContentAwareVersionControl* >(project->versionControlPlugin()); if (!iface) { return; } if ( !qobject_cast( document ) ) { return; } CheckInRepositoryJob* req = iface->isInRepository( document ); if ( !req ) { return; } QObject::connect(req, &CheckInRepositoryJob::finished, q, &TextDocument::repositoryCheckFinished); // Abort the request when the user edits the document QObject::connect(q->textDocument(), &KTextEditor::Document::textChanged, req, &CheckInRepositoryJob::abort); } void modifiedOnDisk(KTextEditor::Document *document, bool /*isModified*/, KTextEditor::ModificationInterface::ModifiedOnDiskReason reason) { bool dirty = false; switch (reason) { case KTextEditor::ModificationInterface::OnDiskUnmodified: break; case KTextEditor::ModificationInterface::OnDiskModified: case KTextEditor::ModificationInterface::OnDiskCreated: case KTextEditor::ModificationInterface::OnDiskDeleted: dirty = true; break; } // In some cases, the VCS (e.g. git) can know whether the old contents are "valuable", i.e. // not retrieveable from the VCS. If that is not the case, then the document can safely be // reloaded without displaying a dialog asking the user. if ( dirty ) { queryCanRecreateFromVcs(document); } setStatus(document, dirty); } void cleanContextMenu() { if (!addedContextMenu) { return; } if (currentContextMenu) { const auto actions = addedContextMenu->actions(); for (QAction* action : actions) { currentContextMenu->removeAction(action); } currentContextMenu.clear(); } // The addedContextMenu owns those actions created on-the-fly for the context menu // (other than those actions only shared for the context menu, but also used elsewhere) // and thuse deletes then on its own destruction. // Some actions potentially could be connected to triggered-signal handlers // using Qt::QueuedConnection (at least SwitchToBuddyPlugin does so currently). // Deleting them here also would also delete the connection before the handler is triggered. // So we delay the menu's and thus their destruction to the next eventloop by default. addedContextMenu->deleteLater(); addedContextMenu = nullptr; } TextDocument * const q; QPointer document; IDocument::DocumentState state = IDocument::Clean; QString encoding; bool loaded = false; // we want to remove the added stuff when the menu hides QMenu* addedContextMenu = nullptr; QPointer currentContextMenu; }; class TextViewPrivate { public: explicit TextViewPrivate(TextView* q) : q(q) {} TextView* const q; QPointer view; KTextEditor::Range initialRange; }; TextDocument::TextDocument(const QUrl &url, ICore* core, const QString& encoding) : PartDocument(url, core) , d_ptr(new TextDocumentPrivate(this)) { Q_D(TextDocument); d->encoding = encoding; } TextDocument::~TextDocument() = default; bool TextDocument::isTextDocument() const { Q_D(const TextDocument); if( !d->document ) { /// @todo Somehow it can happen that d->document is zero, which makes /// code relying on "isTextDocument() == (bool)textDocument()" crash qCWarning(SHELL) << "Broken text-document: " << url(); return false; } return true; } KTextEditor::Document *TextDocument::textDocument() const { Q_D(const TextDocument); return d->document; } QWidget *TextDocument::createViewWidget(QWidget *parent) { Q_D(TextDocument); KTextEditor::View* view = nullptr; if (!d->document) { d->document = Core::self()->partControllerInternal()->createTextPart(); // Connect to the first text changed signal, it occurs before the completed() signal connect(d->document.data(), &KTextEditor::Document::textChanged, this, &TextDocument::slotDocumentLoaded); // Also connect to the completed signal, sometimes the first text changed signal is missed because the part loads too quickly (? TODO - confirm this is necessary) connect(d->document.data(), QOverload<>::of(&KTextEditor::Document::completed), this, &TextDocument::slotDocumentLoaded); // force a reparse when a document gets reloaded connect(d->document.data(), &KTextEditor::Document::reloaded, this, [] (KTextEditor::Document* document) { ICore::self()->languageController()->backgroundParser()->addDocument(IndexedString(document->url()), (TopDUContext::Features) ( TopDUContext::AllDeclarationsContextsAndUses | TopDUContext::ForceUpdate ), BackgroundParser::BestPriority, nullptr); }); // Set encoding passed via constructor // Needs to be done before openUrl, else katepart won't use the encoding // @see KTextEditor::Document::setEncoding if (!d->encoding.isEmpty()) d->document->setEncoding(d->encoding); if (!url().isEmpty() && !DocumentController::isEmptyDocumentUrl(url())) d->document->openUrl( url() ); d->setStatus(d->document, false); /* It appears, that by default a part will be deleted the first view containing it is deleted. Since we do want to have several views, disable that behaviour. */ d->document->setAutoDeletePart(false); Core::self()->partController()->addPart(d->document, false); d->loadSessionConfig(); connect(d->document.data(), &KTextEditor::Document::modifiedChanged, this, &TextDocument::newDocumentStatus); connect(d->document.data(), &KTextEditor::Document::textChanged, this, &TextDocument::textChanged); connect(d->document.data(), &KTextEditor::Document::documentUrlChanged, this, &TextDocument::documentUrlChanged); connect(d->document.data(), &KTextEditor::Document::documentSavedOrUploaded, this, &TextDocument::documentSaved ); if (qobject_cast(d->document)) { // can't use new signal/slot syntax here, MarkInterface is not a QObject connect(d->document.data(), SIGNAL(marksChanged(KTextEditor::Document*)), this, SLOT(saveSessionConfig())); } if (auto iface = qobject_cast(d->document)) { iface->setModifiedOnDiskWarning(true); // can't use new signal/slot syntax here, ModificationInterface is not a QObject connect(d->document.data(), SIGNAL(modifiedOnDisk(KTextEditor::Document*,bool,KTextEditor::ModificationInterface::ModifiedOnDiskReason)), this, SLOT(modifiedOnDisk(KTextEditor::Document*,bool,KTextEditor::ModificationInterface::ModifiedOnDiskReason))); } notifyTextDocumentCreated(); } view = d->document->createView(parent, Core::self()->uiControllerInternal()->defaultMainWindow()->kateWrapper()->interface()); // get rid of some actions regarding the config dialog, we merge that one into the kdevelop menu already delete view->actionCollection()->action(QStringLiteral("set_confdlg")); delete view->actionCollection()->action(QStringLiteral("editor_options")); view->setStatusBarEnabled(Core::self()->partControllerInternal()->showTextEditorStatusBar()); connect(view, &KTextEditor::View::contextMenuAboutToShow, this, &TextDocument::populateContextMenu); if (auto* cc = qobject_cast(view)) cc->setAutomaticInvocationEnabled(core()->languageController()->completionSettings()->automaticCompletionEnabled()); return view; } KParts::Part *TextDocument::partForView(QWidget *view) const { Q_D(const TextDocument); if (d->document && d->document->views().contains((KTextEditor::View*)view)) return d->document; return nullptr; } // KDevelop::IDocument implementation void TextDocument::reload() { Q_D(TextDocument); if (!d->document) return; KTextEditor::ModificationInterface* modif=nullptr; if(d->state ==Dirty) { modif = qobject_cast(d->document); modif->setModifiedOnDiskWarning(false); } d->document->documentReload(); if(modif) modif->setModifiedOnDiskWarning(true); } bool TextDocument::save(DocumentSaveMode mode) { Q_D(TextDocument); if (!d->document) return true; if (mode & Discard) return true; switch (d->state) { case IDocument::Clean: return true; case IDocument::Modified: break; case IDocument::Dirty: case IDocument::DirtyAndModified: if (!(mode & Silent)) { int code = KMessageBox::warningYesNoCancel( Core::self()->uiController()->activeMainWindow(), i18n("The file \"%1\" is modified on disk.\n\nAre " "you sure you want to overwrite it? (External " "changes will be lost.)", d->document->url().toLocalFile()), i18nc("@title:window", "Document Externally Modified")); if (code != KMessageBox::Yes) return false; } break; } if (!KDevelop::ensureWritable(QList() << url())) { return false; } QUrl urlBeforeSave = d->document->url(); if (d->document->documentSave()) { if (d->document->url() != urlBeforeSave) notifyUrlChanged(); return true; } return false; } IDocument::DocumentState TextDocument::state() const { Q_D(const TextDocument); return d->state; } KTextEditor::Cursor KDevelop::TextDocument::cursorPosition() const { Q_D(const TextDocument); if (!d->document) { return KTextEditor::Cursor::invalid(); } KTextEditor::View *view = activeTextView(); if (view) return view->cursorPosition(); return KTextEditor::Cursor::invalid(); } void TextDocument::setCursorPosition(const KTextEditor::Cursor &cursor) { Q_D(TextDocument); if (!cursor.isValid() || !d->document) return; KTextEditor::View *view = activeTextView(); // Rodda: Cursor must be accurate here, to the definition of accurate for KTextEditor::Cursor. // ie, starting from 0,0 if (view) view->setCursorPosition(cursor); } KTextEditor::Range TextDocument::textSelection() const { Q_D(const TextDocument); if (!d->document) { return KTextEditor::Range::invalid(); } KTextEditor::View *view = activeTextView(); if (view && view->selection()) { return view->selectionRange(); } return PartDocument::textSelection(); } QString TextDocument::text(const KTextEditor::Range &range) const { + VERIFY_FOREGROUND_LOCKED Q_D(const TextDocument); if (!d->document) { return QString(); } return d->document->text( range ); } QString TextDocument::textLine() const { + VERIFY_FOREGROUND_LOCKED Q_D(const TextDocument); if (!d->document) { return QString(); } KTextEditor::View *view = activeTextView(); if (view) { return d->document->line( view->cursorPosition().line() ); } return PartDocument::textLine(); } QString TextDocument::textWord() const { + VERIFY_FOREGROUND_LOCKED Q_D(const TextDocument); if (!d->document) { return QString(); } KTextEditor::View *view = activeTextView(); if (view) { KTextEditor::Cursor start = view->cursorPosition(); qCDebug(SHELL) << "got start position from view:" << start.line() << start.column(); QString linestr = textLine(); int startPos = qMax( qMin( start.column(), linestr.length() - 1 ), 0 ); int endPos = startPos; startPos --; while (startPos >= 0 && (linestr[startPos].isLetterOrNumber() || linestr[startPos] == QLatin1Char('_') || linestr[startPos] == QLatin1Char('~'))) { --startPos; } while (endPos < linestr.length() && (linestr[endPos].isLetterOrNumber() || linestr[endPos] == QLatin1Char('_') || linestr[endPos] == QLatin1Char('~'))) { ++endPos; } if( startPos != endPos ) { qCDebug(SHELL) << "found word" << startPos << endPos << linestr.mid( startPos+1, endPos - startPos - 1 ); return linestr.mid( startPos + 1, endPos - startPos - 1 ); } } return PartDocument::textWord(); } void TextDocument::setTextSelection(const KTextEditor::Range &range) { Q_D(TextDocument); if (!range.isValid() || !d->document) return; KTextEditor::View *view = activeTextView(); if (view) { selectAndReveal(view, range); } } Sublime::View* TextDocument::newView(Sublime::Document* doc) { Q_UNUSED(doc); return new TextView(this); } } KDevelop::TextView::TextView(TextDocument * doc) : View(doc, View::TakeOwnership) , d_ptr(new TextViewPrivate(this)) { } KDevelop::TextView::~TextView() = default; QWidget * KDevelop::TextView::createWidget(QWidget * parent) { Q_D(TextView); auto textDocument = qobject_cast(document()); Q_ASSERT(textDocument); QWidget* widget = textDocument->createViewWidget(parent); d->view = qobject_cast(widget); Q_ASSERT(d->view); connect(d->view.data(), &KTextEditor::View::cursorPositionChanged, this, &KDevelop::TextView::sendStatusChanged); return widget; } void KDevelop::TextView::setInitialRange(const KTextEditor::Range& range) { Q_D(TextView); if (d->view) { selectAndReveal(d->view, range); } else { d->initialRange = range; } } KTextEditor::Range KDevelop::TextView::initialRange() const { Q_D(const TextView); return d->initialRange; } void KDevelop::TextView::readSessionConfig(KConfigGroup& config) { Q_D(TextView); if (!d->view) { return; } d->view->readSessionConfig(config); } void KDevelop::TextView::writeSessionConfig(KConfigGroup& config) { Q_D(TextView); if (!d->view) { return; } d->view->writeSessionConfig(config); } QString KDevelop::TextDocument::documentType() const { return QStringLiteral("Text"); } QIcon KDevelop::TextDocument::defaultIcon() const { Q_D(const TextDocument); if (d->document) { QMimeType mime = QMimeDatabase().mimeTypeForName(d->document->mimeType()); QIcon icon = QIcon::fromTheme(mime.iconName()); if (!icon.isNull()) { return icon; } } return PartDocument::defaultIcon(); } KTextEditor::View *KDevelop::TextView::textView() const { Q_D(const TextView); return d->view; } QString KDevelop::TextView::viewStatus() const { Q_D(const TextView); // only show status when KTextEditor's own status bar isn't already enabled const bool showStatus = !Core::self()->partControllerInternal()->showTextEditorStatusBar(); if (!showStatus) { return QString(); } const KTextEditor::Cursor pos = d->view ? d->view->cursorPosition() : KTextEditor::Cursor::invalid(); return i18n(" Line: %1 Col: %2 ", pos.line() + 1, pos.column() + 1); } void KDevelop::TextView::sendStatusChanged() { emit statusChanged(this); } KTextEditor::View* KDevelop::TextDocument::activeTextView() const { KTextEditor::View* fallback = nullptr; for (auto view : views()) { auto textView = qobject_cast(view)->textView(); if (!textView) { continue; } if (textView->hasFocus()) { return textView; } else if (textView->isVisible()) { fallback = textView; } else if (!fallback) { fallback = textView; } } return fallback; } void KDevelop::TextDocument::newDocumentStatus(KTextEditor::Document *document) { Q_D(TextDocument); bool dirty = (d->state == IDocument::Dirty || d->state == IDocument::DirtyAndModified); d->setStatus(document, dirty); } void KDevelop::TextDocument::textChanged(KTextEditor::Document *document) { Q_UNUSED(document); notifyContentChanged(); } void KDevelop::TextDocument::unpopulateContextMenu() { Q_D(TextDocument); auto* menu = qobject_cast(sender()); disconnect(menu, &QMenu::aboutToHide, this, &TextDocument::unpopulateContextMenu); d->cleanContextMenu(); } void KDevelop::TextDocument::populateContextMenu( KTextEditor::View* v, QMenu* menu ) { Q_D(TextDocument); if (d->addedContextMenu) { qCWarning(SHELL) << "populateContextMenu() called while we still handled another menu."; d->cleanContextMenu(); } d->currentContextMenu = menu; connect(menu, &QMenu::aboutToHide, this, &TextDocument::unpopulateContextMenu); d->addedContextMenu = new QMenu(); EditorContext c(v, v->cursorPosition()); auto extensions = Core::self()->pluginController()->queryPluginsForContextMenuExtensions(&c, d->addedContextMenu); ContextMenuExtension::populateMenu(d->addedContextMenu, extensions); const auto actions = d->addedContextMenu->actions(); for (QAction* action : actions) { menu->addAction(action); } } void KDevelop::TextDocument::repositoryCheckFinished(bool canRecreate) { Q_D(TextDocument); if ( d->state != IDocument::Dirty && d->state != IDocument::DirtyAndModified ) { // document is not dirty for whatever reason, nothing to do. return; } if ( ! canRecreate ) { return; } KTextEditor::ModificationInterface* modIface = qobject_cast( d->document ); Q_ASSERT(modIface); // Ok, all safe, we can clean up the document. Close it if the file is gone, // and reload if it's still there. d->setStatus(d->document, false); modIface->setModifiedOnDisk(KTextEditor::ModificationInterface::OnDiskUnmodified); if ( QFile::exists(d->document->url().path()) ) { reload(); } else { close(KDevelop::IDocument::Discard); } } void KDevelop::TextDocument::slotDocumentLoaded() { Q_D(TextDocument); if (d->loaded) return; // Tell the editor integrator first d->loaded = true; notifyLoaded(); } void KDevelop::TextDocument::documentSaved(KTextEditor::Document* document, bool saveAs) { Q_UNUSED(document); Q_UNUSED(saveAs); notifySaved(); notifyStateChanged(); } void KDevelop::TextDocument::documentUrlChanged(KTextEditor::Document* document) { Q_D(TextDocument); Q_UNUSED(document); if (url() != d->document->url()) setUrl(d->document->url()); } #include "moc_textdocument.cpp" diff --git a/kdevplatform/util/foregroundlock.cpp b/kdevplatform/util/foregroundlock.cpp index 5539d0d132..8e1e23b224 100644 --- a/kdevplatform/util/foregroundlock.cpp +++ b/kdevplatform/util/foregroundlock.cpp @@ -1,245 +1,246 @@ /* Copyright 2010 David Nolden This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #include "foregroundlock.h" #include #include #include using namespace KDevelop; namespace { QMutex internalMutex; QMutex tryLockMutex; QMutex waitMutex; QMutex finishMutex; QWaitCondition condition; volatile QThread* holderThread = nullptr; volatile int recursion = 0; void lockForegroundMutexInternal() { if (holderThread == QThread::currentThread()) { // We already have the mutex ++recursion; } else { internalMutex.lock(); Q_ASSERT(recursion == 0 && holderThread == nullptr); holderThread = QThread::currentThread(); recursion = 1; } } bool tryLockForegroundMutexInternal(int interval = 0) { if (holderThread == QThread::currentThread()) { // We already have the mutex ++recursion; return true; } else { if (internalMutex.tryLock(interval)) { Q_ASSERT(recursion == 0 && holderThread == nullptr); holderThread = QThread::currentThread(); recursion = 1; return true; } else { return false; } } } void unlockForegroundMutexInternal(bool duringDestruction = false) { /// Note: QThread::currentThread() might already be invalid during destruction. if (!duringDestruction) { Q_ASSERT(holderThread == QThread::currentThread()); } Q_ASSERT(recursion > 0); recursion -= 1; if (recursion == 0) { holderThread = nullptr; internalMutex.unlock(); } } } ForegroundLock::ForegroundLock(bool lock) { if (lock) relock(); } void KDevelop::ForegroundLock::relock() { Q_ASSERT(!m_locked); if (!QApplication::instance() || // Initialization isn't complete yet QThread::currentThread() == QApplication::instance()->thread() || // We're the main thread (deadlock might happen if we'd enter the trylock loop) holderThread == QThread::currentThread()) { // We already have the foreground lock (deadlock might happen if we'd enter the trylock loop) lockForegroundMutexInternal(); } else { QMutexLocker lock(&tryLockMutex); while (!tryLockForegroundMutexInternal(10)) { // In case an additional event-loop was started from within the foreground, we send // events to the foreground to temporarily release the lock. class ForegroundReleaser : public DoInForeground { public: void doInternal() override { // By locking the mutex, we make sure that the requester is actually waiting for the condition waitMutex.lock(); // Now we release the foreground lock TemporarilyReleaseForegroundLock release; // And signalize to the requester that we've released it condition.wakeAll(); // Allow the requester to actually wake up, by unlocking m_waitMutex waitMutex.unlock(); // Now wait until the requester is ready QMutexLocker lock(&finishMutex); } }; static ForegroundReleaser releaser; QMutexLocker lockWait(&waitMutex); QMutexLocker lockFinish(&finishMutex); QMetaObject::invokeMethod(&releaser, "doInternalSlot", Qt::QueuedConnection); // We limit the waiting time here, because sometimes it may happen that the foreground-lock is released, // and the foreground is waiting without an event-loop running. (For example through TemporarilyReleaseForegroundLock) condition.wait(&waitMutex, 30); if (tryLockForegroundMutexInternal()) { //success break; } else { //Probably a third thread has creeped in and //got the foreground lock before us. Just try again. } } } m_locked = true; Q_ASSERT(holderThread == QThread::currentThread()); Q_ASSERT(recursion > 0); } bool KDevelop::ForegroundLock::isLockedForThread() { - return QThread::currentThread() == holderThread; + return QThread::currentThread() == holderThread || + QThread::currentThread() == QApplication::instance()->thread(); } bool KDevelop::ForegroundLock::tryLock() { if (tryLockForegroundMutexInternal()) { m_locked = true; return true; } return false; } void KDevelop::ForegroundLock::unlock() { Q_ASSERT(m_locked); unlockForegroundMutexInternal(); m_locked = false; } TemporarilyReleaseForegroundLock::TemporarilyReleaseForegroundLock() { Q_ASSERT(holderThread == QThread::currentThread()); m_recursion = 0; while (holderThread == QThread::currentThread()) { unlockForegroundMutexInternal(); ++m_recursion; } } TemporarilyReleaseForegroundLock::~TemporarilyReleaseForegroundLock() { for (int a = 0; a < m_recursion; ++a) lockForegroundMutexInternal(); Q_ASSERT(recursion == m_recursion && holderThread == QThread::currentThread()); } KDevelop::ForegroundLock::~ForegroundLock() { if (m_locked) unlock(); } bool KDevelop::ForegroundLock::isLocked() const { return m_locked; } namespace KDevelop { void DoInForeground::doIt() { if (QThread::currentThread() == QApplication::instance()->thread()) { // We're already in the foreground, just call the handler code doInternal(); } else { QMutexLocker lock(&m_mutex); QMetaObject::invokeMethod(this, "doInternalSlot", Qt::QueuedConnection); m_wait.wait(&m_mutex); } } DoInForeground::~DoInForeground() { } DoInForeground::DoInForeground() { moveToThread(QApplication::instance()->thread()); } void DoInForeground::doInternalSlot() { VERIFY_FOREGROUND_LOCKED doInternal(); QMutexLocker lock(&m_mutex); m_wait.wakeAll(); } } // Important: The foreground lock has to be held by default, so lock it during static initialization static struct StaticLock { StaticLock() { lockForegroundMutexInternal(); } ~StaticLock() { unlockForegroundMutexInternal(true); } private: Q_DISABLE_COPY(StaticLock) } staticLock; diff --git a/plugins/clang/duchain/clangproblem.cpp b/plugins/clang/duchain/clangproblem.cpp index 35dbe66d99..26bcb12498 100644 --- a/plugins/clang/duchain/clangproblem.cpp +++ b/plugins/clang/duchain/clangproblem.cpp @@ -1,293 +1,290 @@ /* * Copyright 2014 Kevin Funk * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ #include "clangproblem.h" #include #include #include "util/clangtypes.h" #include "util/clangdebug.h" +#include "util/clangutils.h" #include #include #include using namespace KDevelop; namespace { IProblem::Severity diagnosticSeverityToSeverity(CXDiagnosticSeverity severity, const QString& optionName) { switch (severity) { case CXDiagnostic_Fatal: case CXDiagnostic_Error: return IProblem::Error; case CXDiagnostic_Warning: if (optionName.startsWith(QLatin1String("-Wunused-"))) { return IProblem::Hint; } return IProblem::Warning; default: return IProblem::Hint; } } /** * Clang diagnostic messages always start with a lowercase character * * @return Prettified version, starting with uppercase character */ inline QString prettyDiagnosticSpelling(const QString& str) { QString ret = str; if (ret.isEmpty()) { return {}; } ret[0] = ret[0].toUpper(); return ret; } -ClangFixits fixitsForDiagnostic(CXDiagnostic diagnostic) +ClangFixits fixitsForDiagnostic(CXDiagnostic diagnostic, CXTranslationUnit unit) { ClangFixits fixits; auto numFixits = clang_getDiagnosticNumFixIts(diagnostic); fixits.reserve(numFixits); for (uint i = 0; i < numFixits; ++i) { CXSourceRange range; const QString replacementText = ClangString(clang_getDiagnosticFixIt(diagnostic, i, &range)).toString(); - - const auto docRange = ClangRange(range).toDocumentRange(); - auto doc = KDevelop::ICore::self()->documentController()->documentForUrl(docRange.document.toUrl()); - const QString original = doc ? doc->text(docRange) : QString{}; - - fixits << ClangFixit{replacementText, docRange, QString(), original}; + QByteArray original = ClangUtils::getRawContents(unit, range); + fixits << ClangFixit{replacementText, ClangRange(range).toDocumentRange(), QString(), QString::fromLocal8Bit(original)}; } return fixits; } } QDebug operator<<(QDebug debug, const ClangFixit& fixit) { debug.nospace() << "ClangFixit[" << "replacementText=" << fixit.replacementText << ", range=" << fixit.range << ", description=" << fixit.description << "]"; return debug; } ClangProblem::ClangProblem() = default; ClangProblem::ClangProblem(const ClangProblem& other) : Problem(), m_fixits(other.m_fixits) { setSource(other.source()); setFinalLocation(other.finalLocation()); setFinalLocationMode(other.finalLocationMode()); setDescription(other.description()); setExplanation(other.explanation()); setSeverity(other.severity()); auto diagnostics = other.diagnostics(); for (auto& diagnostic : diagnostics) { auto* clangDiagnostic = dynamic_cast(diagnostic.data()); Q_ASSERT(clangDiagnostic); diagnostic = ClangProblem::Ptr(new ClangProblem(*clangDiagnostic)); } setDiagnostics(diagnostics); } ClangProblem::ClangProblem(CXDiagnostic diagnostic, CXTranslationUnit unit) { const QString diagnosticOption = ClangString(clang_getDiagnosticOption(diagnostic, nullptr)).toString(); auto severity = diagnosticSeverityToSeverity(clang_getDiagnosticSeverity(diagnostic), diagnosticOption); setSeverity(severity); QString description = ClangString(clang_getDiagnosticSpelling(diagnostic)).toString(); if (!diagnosticOption.isEmpty()) { description.append(QLatin1String(" [") + diagnosticOption + QLatin1Char(']')); } setDescription(prettyDiagnosticSpelling(description)); ClangLocation location(clang_getDiagnosticLocation(diagnostic)); CXFile diagnosticFile; clang_getFileLocation(location, &diagnosticFile, nullptr, nullptr, nullptr); const ClangString fileName(clang_getFileName(diagnosticFile)); DocumentRange docRange(IndexedString(QUrl::fromLocalFile(fileName.toString()).adjusted(QUrl::NormalizePathSegments)), KTextEditor::Range(location, location)); const uint numRanges = clang_getDiagnosticNumRanges(diagnostic); for (uint i = 0; i < numRanges; ++i) { auto range = ClangRange(clang_getDiagnosticRange(diagnostic, i)).toRange(); // Note that the second condition is a workaround for seemingly wrong ranges that // were observed sometimes. In principle, such a range should be valid. if(!range.isValid() || (range.isEmpty() && range.start().line() == 0 && range.start().column() == 0)){ continue; } if (range.start() < docRange.start()) { docRange.setStart(range.start()); } if (range.end() > docRange.end()) { docRange.setEnd(range.end()); } } if (docRange.isEmpty()) { // try to find a bigger range for the given location by using the token at the given location CXFile file = nullptr; unsigned line = 0; unsigned column = 0; clang_getExpansionLocation(location, &file, &line, &column, nullptr); // just skip ahead some characters, hoping that it's sufficient to encompass // a token we can use for building the range auto nextLocation = clang_getLocation(unit, file, line, column + 100); auto rangeToTokenize = clang_getRange(location, nextLocation); const ClangTokens tokens(unit, rangeToTokenize); if (tokens.size()) { docRange.setRange(ClangRange(clang_getTokenExtent(unit, tokens.at(0))).toRange()); } } - setFixits(fixitsForDiagnostic(diagnostic)); + setFixits(fixitsForDiagnostic(diagnostic, unit)); setFinalLocation(docRange); setSource(IProblem::SemanticAnalysis); QVector diagnostics; auto childDiagnostics = clang_getChildDiagnostics(diagnostic); auto numChildDiagnostics = clang_getNumDiagnosticsInSet(childDiagnostics); diagnostics.reserve(numChildDiagnostics); for (uint j = 0; j < numChildDiagnostics; ++j) { auto childDiagnostic = clang_getDiagnosticInSet(childDiagnostics, j); ClangProblem::Ptr problem(new ClangProblem(childDiagnostic, unit)); diagnostics << ProblemPointer(problem.data()); } setDiagnostics(diagnostics); } IAssistant::Ptr ClangProblem::solutionAssistant() const { if (allFixits().isEmpty()) { return {}; } return IAssistant::Ptr(new ClangFixitAssistant(allFixits())); } ClangFixits ClangProblem::fixits() const { return m_fixits; } void ClangProblem::setFixits(const ClangFixits& fixits) { m_fixits = fixits; } ClangFixits ClangProblem::allFixits() const { ClangFixits result; result << m_fixits; const auto& diagnostics = this->diagnostics(); for (const IProblem::Ptr& diagnostic : diagnostics) { const Ptr problem(dynamic_cast(diagnostic.data())); Q_ASSERT(problem); result << problem->allFixits(); } return result; } ClangFixitAssistant::ClangFixitAssistant(const ClangFixits& fixits) : m_title(i18n("Fix-it Hints")) , m_fixits(fixits) { } ClangFixitAssistant::ClangFixitAssistant(const QString& title, const ClangFixits& fixits) : m_title(title) , m_fixits(fixits) { } QString ClangFixitAssistant::title() const { return m_title; } void ClangFixitAssistant::createActions() { KDevelop::IAssistant::createActions(); for (const ClangFixit& fixit : qAsConst(m_fixits)) { addAction(IAssistantAction::Ptr(new ClangFixitAction(fixit))); } } ClangFixits ClangFixitAssistant::fixits() const { return m_fixits; } ClangFixitAction::ClangFixitAction(const ClangFixit& fixit) : m_fixit(fixit) { } QString ClangFixitAction::description() const { if (!m_fixit.description.isEmpty()) return m_fixit.description; const auto range = m_fixit.range; if (range.start() == range.end()) { return i18n("Insert \"%1\" at line: %2, column: %3", m_fixit.replacementText, range.start().line()+1, range.start().column()+1); } else if (range.start().line() == range.end().line()) { if (m_fixit.currentText.isEmpty()) { return i18n("Replace text at line: %1, column: %2 with: \"%3\"", range.start().line()+1, range.start().column()+1, m_fixit.replacementText); } else return i18n("Replace \"%1\" with: \"%2\"", m_fixit.currentText, m_fixit.replacementText); } else { return i18n("Replace multiple lines starting at line: %1, column: %2 with: \"%3\"", range.start().line()+1, range.start().column()+1, m_fixit.replacementText); } } void ClangFixitAction::execute() { DocumentChangeSet changes; { DUChainReadLocker lock; DocumentChange change(m_fixit.range.document, m_fixit.range, m_fixit.currentText, m_fixit.replacementText); change.m_ignoreOldText = !m_fixit.currentText.isEmpty(); changes.addChange(change); } changes.setReplacementPolicy(DocumentChangeSet::WarnOnFailedChange); changes.applyAllChanges(); emit executed(this); }