diff --git a/language/codecompletion/codecompletionmodel.cpp b/language/codecompletion/codecompletionmodel.cpp index 4d6247c6b..094992910 100644 --- a/language/codecompletion/codecompletionmodel.cpp +++ b/language/codecompletion/codecompletionmodel.cpp @@ -1,454 +1,449 @@ /* * KDevelop Generic Code Completion Support * * Copyright 2006-2008 Hamish Rodda * Copyright 2007-2008 David Nolden * * 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 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 "codecompletionmodel.h" #include #include #include #include #include #include #include #include -#include #include "../duchain/declaration.h" #include "../duchain/classfunctiondeclaration.h" #include "../duchain/ducontext.h" #include "../duchain/duchain.h" #include "../duchain/namespacealiasdeclaration.h" #include "../duchain/parsingenvironment.h" #include "../duchain/duchainlock.h" #include "../duchain/duchainbase.h" #include "../duchain/topducontext.h" #include "../duchain/duchainutils.h" #include "../interfaces/quickopendataprovider.h" #include "../interfaces/icore.h" #include "../interfaces/ilanguagecontroller.h" #include "../interfaces/icompletionsettings.h" #include "util/debug.h" #include "codecompletionworker.h" #include "codecompletioncontext.h" #include -#if KTEXTEDITOR_VERSION < QT_VERSION_CHECK(5, 10, 0) -Q_DECLARE_METATYPE(KTextEditor::Cursor) -#endif - using namespace KTextEditor; //Multi-threaded completion creates some multi-threading related crashes, and sometimes shows the completions in the wrong position if the cursor was moved // #define SINGLE_THREADED_COMPLETION namespace KDevelop { class CompletionWorkerThread : public QThread { Q_OBJECT public: explicit CompletionWorkerThread(CodeCompletionModel* model) : QThread(model), m_model(model), m_worker(m_model->createCompletionWorker()) { Q_ASSERT(m_worker->parent() == nullptr); // Must be null, else we cannot change the thread affinity! m_worker->moveToThread(this); Q_ASSERT(m_worker->thread() == this); } ~CompletionWorkerThread() override { delete m_worker; } void run () override { //We connect directly, so we can do the pre-grouping within the background thread connect(m_worker, &CodeCompletionWorker::foundDeclarationsReal, m_model, &CodeCompletionModel::foundDeclarations, Qt::QueuedConnection); connect(m_model, &CodeCompletionModel::completionsNeeded, m_worker, static_cast,const Cursor&,View*)>(&CodeCompletionWorker::computeCompletions), Qt::QueuedConnection); connect(m_model, &CodeCompletionModel::doSpecialProcessingInBackground, m_worker, &CodeCompletionWorker::doSpecialProcessing); exec(); } CodeCompletionModel* m_model; CodeCompletionWorker* m_worker; }; bool CodeCompletionModel::forceWaitForModel() { return m_forceWaitForModel; } void CodeCompletionModel::setForceWaitForModel(bool wait) { m_forceWaitForModel = wait; } CodeCompletionModel::CodeCompletionModel( QObject * parent ) : KTextEditor::CodeCompletionModel(parent) , m_forceWaitForModel(false) , m_fullCompletion(true) , m_mutex(new QMutex) , m_thread(nullptr) { qRegisterMetaType(); } void CodeCompletionModel::initialize() { if(!m_thread) { m_thread = new CompletionWorkerThread(this); #ifdef SINGLE_THREADED_COMPLETION m_thread->m_worker = createCompletionWorker(); #endif m_thread->start(); } } CodeCompletionModel::~CodeCompletionModel() { if(m_thread->m_worker) m_thread->m_worker->abortCurrentCompletion(); m_thread->quit(); m_thread->wait(); delete m_thread; delete m_mutex; } void CodeCompletionModel::addNavigationWidget(const CompletionTreeElement* element, QWidget* widget) const { Q_ASSERT(dynamic_cast(widget)); m_navigationWidgets[element] = widget; } bool CodeCompletionModel::fullCompletion() const { return m_fullCompletion; } KDevelop::CodeCompletionWorker* CodeCompletionModel::worker() const { return m_thread->m_worker; } void CodeCompletionModel::clear() { beginResetModel(); m_completionItems.clear(); m_navigationWidgets.clear(); m_completionContext.reset(); endResetModel(); } void CodeCompletionModel::completionInvokedInternal(KTextEditor::View* view, const KTextEditor::Range& range, InvocationType invocationType, const QUrl& url) { Q_ASSERT(m_thread == worker()->thread()); Q_UNUSED(invocationType) DUChainReadLocker lock(DUChain::lock(), 400); if( !lock.locked() ) { qCDebug(LANGUAGE) << "could not lock du-chain in time"; return; } TopDUContext* top = DUChainUtils::standardContextForUrl( url ); if(!top) { return; } setCurrentTopContext(TopDUContextPointer(top)); RangeInRevision rangeInRevision = top->transformToLocalRevision(KTextEditor::Range(range)); if (top) { qCDebug(LANGUAGE) << "completion invoked for context" << (DUContext*)top; if( top->parsingEnvironmentFile() && top->parsingEnvironmentFile()->modificationRevision() != ModificationRevision::revisionForFile(IndexedString(url.toString())) ) { qCDebug(LANGUAGE) << "Found context is not current."; } DUContextPointer thisContext; { qCDebug(LANGUAGE) << "apply specialization:" << range.start(); thisContext = SpecializationStore::self().applySpecialization(top->findContextAt(rangeInRevision.start), top); if ( thisContext ) { qCDebug(LANGUAGE) << "after specialization:" << thisContext->localScopeIdentifier().toString() << thisContext->rangeInCurrentRevision(); } if(!thisContext) thisContext = top; qCDebug(LANGUAGE) << "context is set to" << thisContext.data(); if( !thisContext ) { qCDebug(LANGUAGE) << "================== NO CONTEXT FOUND ======================="; beginResetModel(); m_completionItems.clear(); m_navigationWidgets.clear(); endResetModel(); return; } } lock.unlock(); if(m_forceWaitForModel) emit waitForReset(); emit completionsNeeded(thisContext, range.start(), view); } else { qCDebug(LANGUAGE) << "Completion invoked for unknown context. Document:" << url << ", Known documents:" << DUChain::self()->documents(); } } void CodeCompletionModel::completionInvoked(KTextEditor::View* view, const KTextEditor::Range& range, InvocationType invocationType) { //If this triggers, initialize() has not been called after creation. Q_ASSERT(m_thread); KDevelop::ICompletionSettings::CompletionLevel level = KDevelop::ICore::self()->languageController()->completionSettings()->completionLevel(); if(level == KDevelop::ICompletionSettings::AlwaysFull || (invocationType != AutomaticInvocation && level == KDevelop::ICompletionSettings::MinimalWhenAutomatic)) m_fullCompletion = true; else m_fullCompletion = false; //Only use grouping in full completion mode setHasGroups(m_fullCompletion); Q_UNUSED(invocationType) if (!worker()) { qCWarning(LANGUAGE) << "Completion invoked on a completion model which has no code completion worker assigned!"; } beginResetModel(); m_navigationWidgets.clear(); m_completionItems.clear(); endResetModel(); worker()->abortCurrentCompletion(); worker()->setFullCompletion(m_fullCompletion); QUrl url = view->document()->url(); completionInvokedInternal(view, range, invocationType, url); } void CodeCompletionModel::foundDeclarations(const QList>& items, const QExplicitlySharedDataPointer& completionContext) { m_completionContext = completionContext; if(m_completionItems.isEmpty() && items.isEmpty()) { if(m_forceWaitForModel) { // TODO KF5: Check if this actually works beginResetModel(); endResetModel(); //If we need to reset the model, reset it } return; //We don't need to reset, which is bad for target model } beginResetModel(); m_completionItems = items; endResetModel(); if(m_completionContext) { qCDebug(LANGUAGE) << "got completion-context with " << m_completionContext->ungroupedElements().size() << "ungrouped elements"; } } KTextEditor::CodeCompletionModelControllerInterface::MatchReaction CodeCompletionModel::matchingItem(const QModelIndex& /*matched*/) { return None; } void CodeCompletionModel::setCompletionContext(QExplicitlySharedDataPointer completionContext) { QMutexLocker lock(m_mutex); m_completionContext = completionContext; if(m_completionContext) { qCDebug(LANGUAGE) << "got completion-context with " << m_completionContext->ungroupedElements().size() << "ungrouped elements"; } } QExplicitlySharedDataPointer CodeCompletionModel::completionContext() const { QMutexLocker lock(m_mutex); return m_completionContext; } void CodeCompletionModel::executeCompletionItem(View* view, const KTextEditor::Range& word, const QModelIndex& index) const { //We must not lock the duchain at this place, because the items might rely on that CompletionTreeElement* element = static_cast(index.internalPointer()); if( !element || !element->asItem() ) return; element->asItem()->execute(view, word); } QExplicitlySharedDataPointer< KDevelop::CompletionTreeElement > CodeCompletionModel::itemForIndex(QModelIndex index) const { CompletionTreeElement* element = static_cast(index.internalPointer()); return QExplicitlySharedDataPointer< KDevelop::CompletionTreeElement >(element); } QVariant CodeCompletionModel::data(const QModelIndex& index, int role) const { if ( role == Qt::TextAlignmentRole && index.column() == 0 ) { return Qt::AlignRight; } auto element = static_cast(index.internalPointer()); if( !element ) return QVariant(); if( role == CodeCompletionModel::GroupRole ) { if( element->asNode() ) { return QVariant(element->asNode()->role); }else { qCDebug(LANGUAGE) << "Requested group-role from leaf tree element"; return QVariant(); } }else{ if( element->asNode() ) { if( role == CodeCompletionModel::InheritanceDepth ) { auto customGroupNode = dynamic_cast(element); if(customGroupNode) return QVariant(customGroupNode->inheritanceDepth); } if( role == element->asNode()->role ) { return element->asNode()->roleValue; } else { return QVariant(); } } } if(!element->asItem()) { qCWarning(LANGUAGE) << "Error in completion model"; return QVariant(); } //Navigation widget interaction is done here, the other stuff is done within the tree-elements switch (role) { case CodeCompletionModel::InheritanceDepth: return element->asItem()->inheritanceDepth(); case CodeCompletionModel::ArgumentHintDepth: return element->asItem()->argumentHintDepth(); case CodeCompletionModel::ItemSelected: { DeclarationPointer decl = element->asItem()->declaration(); if(decl) { DUChain::self()->emitDeclarationSelected(decl); } break; } } //In minimal completion mode, hide all columns except the "name" one if(!m_fullCompletion && role == Qt::DisplayRole && index.column() != Name && (element->asItem()->argumentHintDepth() == 0 || index.column() == Prefix)) return QVariant(); //In reduced completion mode, don't show information text with the selected items if(role == ItemSelected && (!m_fullCompletion || !ICore::self()->languageController()->completionSettings()->showMultiLineSelectionInformation())) return QVariant(); return element->asItem()->data(index, role, this); } KDevelop::TopDUContextPointer CodeCompletionModel::currentTopContext() const { return m_currentTopContext; } void CodeCompletionModel::setCurrentTopContext(KDevelop::TopDUContextPointer topContext) { m_currentTopContext = topContext; } QModelIndex CodeCompletionModel::index(int row, int column, const QModelIndex& parent) const { if( parent.isValid() ) { CompletionTreeElement* element = static_cast(parent.internalPointer()); CompletionTreeNode* node = element->asNode(); if( !node ) { qCDebug(LANGUAGE) << "Requested sub-index of leaf node"; return QModelIndex(); } if (row < 0 || row >= node->children.count() || column < 0 || column >= ColumnCount) return QModelIndex(); return createIndex(row, column, node->children[row].data()); } else { if (row < 0 || row >= m_completionItems.count() || column < 0 || column >= ColumnCount) return QModelIndex(); return createIndex(row, column, const_cast(m_completionItems[row].data())); } } QModelIndex CodeCompletionModel::parent ( const QModelIndex & index ) const { if(rowCount() == 0) return QModelIndex(); if( index.isValid() ) { CompletionTreeElement* element = static_cast(index.internalPointer()); if( element->parent() ) return createIndex( element->rowInParent(), element->columnInParent(), element->parent() ); } return QModelIndex(); } int CodeCompletionModel::rowCount ( const QModelIndex & parent ) const { if( parent.isValid() ) { CompletionTreeElement* element = static_cast(parent.internalPointer()); CompletionTreeNode* node = element->asNode(); if( !node ) return 0; return node->children.count(); }else{ return m_completionItems.count(); } } QString CodeCompletionModel::filterString(KTextEditor::View* view, const KTextEditor::Range& range, const KTextEditor::Cursor& position) { m_filterString = KTextEditor::CodeCompletionModelControllerInterface::filterString(view, range, position); return m_filterString; } } #include "moc_codecompletionmodel.cpp" #include "codecompletionmodel.moc" diff --git a/language/codegen/applychangeswidget.cpp b/language/codegen/applychangeswidget.cpp index ad3d8b3d3..9c9751a30 100644 --- a/language/codegen/applychangeswidget.cpp +++ b/language/codegen/applychangeswidget.cpp @@ -1,214 +1,209 @@ /* Copyright 2008 Aleix Pol * Copyright 2009 Ramón Zarazúa * * 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 "applychangeswidget.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "coderepresentation.h" #include #include -#include -#if KTEXTEDITOR_VERSION < QT_VERSION_CHECK(5, 10, 0) -Q_DECLARE_METATYPE(KTextEditor::Range) -#endif - namespace KDevelop { class ApplyChangesWidgetPrivate { public: explicit ApplyChangesWidgetPrivate(ApplyChangesWidget * p) : parent(p), m_index(0) {} ~ApplyChangesWidgetPrivate() { qDeleteAll(m_temps); } void createEditPart(const KDevelop::IndexedString& url); ApplyChangesWidget * const parent; int m_index; QList m_editParts; QList m_temps; QList m_files; QTabWidget * m_documentTabs; QLabel* m_info; }; ApplyChangesWidget::ApplyChangesWidget(QWidget* parent) : QDialog(parent), d(new ApplyChangesWidgetPrivate(this)) { setSizeGripEnabled(true); auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok|QDialogButtonBox::Cancel); auto mainLayout = new QVBoxLayout(this); auto okButton = buttonBox->button(QDialogButtonBox::Ok); okButton->setDefault(true); okButton->setShortcut(Qt::CTRL | Qt::Key_Return); connect(buttonBox, &QDialogButtonBox::accepted, this, &ApplyChangesWidget::accept); connect(buttonBox, &QDialogButtonBox::rejected, this, &ApplyChangesWidget::reject); QWidget* w=new QWidget(this); d->m_info=new QLabel(w); d->m_documentTabs = new QTabWidget(w); connect(d->m_documentTabs, &QTabWidget::currentChanged, this, &ApplyChangesWidget::indexChanged); QVBoxLayout* l = new QVBoxLayout(w); l->addWidget(d->m_info); l->addWidget(d->m_documentTabs); mainLayout->addWidget(w); mainLayout->addWidget(buttonBox); resize(QSize(800, 400)); } ApplyChangesWidget::~ApplyChangesWidget() { delete d; } bool ApplyChangesWidget::hasDocuments() const { return d->m_editParts.size() > 0; } KTextEditor::Document* ApplyChangesWidget::document() const { return qobject_cast(d->m_editParts[d->m_index]); } void ApplyChangesWidget::setInformation(const QString & info) { d->m_info->setText(info); } void ApplyChangesWidget::addDocuments(const IndexedString & original) { int idx=d->m_files.indexOf(original); if(idx<0) { QWidget * w = new QWidget; d->m_documentTabs->addTab(w, original.str()); d->m_documentTabs->setCurrentWidget(w); d->m_files.insert(d->m_index, original); d->createEditPart(original); } else { d->m_index=idx; } } bool ApplyChangesWidget::applyAllChanges() { /// @todo implement safeguard in case a file saving fails bool ret = true; for(int i = 0; i < d->m_files.size(); ++i ) if(d->m_editParts[i]->saveAs(d->m_files[i].toUrl())) { IDocument* doc = ICore::self()->documentController()->documentForUrl(d->m_files[i].toUrl()); if(doc && doc->state()==IDocument::Dirty) doc->reload(); } else ret = false; return ret; } } namespace KDevelop { void ApplyChangesWidgetPrivate::createEditPart(const IndexedString & file) { QWidget * widget = m_documentTabs->currentWidget(); Q_ASSERT(widget); QVBoxLayout *m=new QVBoxLayout(widget); QSplitter *v=new QSplitter(widget); m->addWidget(v); QUrl url = file.toUrl(); QMimeType mimetype = QMimeDatabase().mimeTypeForUrl(url); KParts::ReadWritePart* part=KMimeTypeTrader::self()->createPartInstanceFromQuery(mimetype.name(), widget, widget); KTextEditor::Document* document=qobject_cast(part); Q_ASSERT(document); Q_ASSERT(document->action("file_save")); document->action("file_save")->setEnabled(false); m_editParts.insert(m_index, part); //Open the best code representation, even if it is artificial CodeRepresentation::Ptr repr = createCodeRepresentation(file); if(!repr->fileExists()) { const QString templateName = QDir::tempPath() + QLatin1Char('/') + url.fileName().split('.').last(); QTemporaryFile * temp(new QTemporaryFile(templateName)); temp->open(); temp->write(repr->text().toUtf8()); temp->close(); url = QUrl::fromLocalFile(temp->fileName()); m_temps << temp; } m_editParts[m_index]->openUrl(url); v->addWidget(m_editParts[m_index]->widget()); v->setSizes(QList() << 400 << 100); } void ApplyChangesWidget::indexChanged(int newIndex) { Q_ASSERT(newIndex != -1); d->m_index = newIndex; } void ApplyChangesWidget::updateDiffView(int index) { int prevIndex = d->m_index; d->m_index = index == -1 ? d->m_index : index; d->m_index = prevIndex; } } diff --git a/util/path.cpp b/util/path.cpp index 53650f438..0b5236b1a 100644 --- a/util/path.cpp +++ b/util/path.cpp @@ -1,518 +1,514 @@ /* * This file is part of KDevelop * Copyright 2012 Milian Wolff * Copyright 2015 Kevin Funk * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 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 6 of version 3 of the license. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library. If not, see . */ #include "path.h" #include #include #include #include using namespace KDevelop; namespace { inline bool isWindowsDriveLetter(const QString& segment) { #ifdef Q_OS_WIN return segment.size() == 2 && segment.at(0).isLetter() && segment.at(1) == ':'; #else Q_UNUSED(segment); return false; #endif } inline bool isAbsolutePath(const QString& path) { if (path.startsWith('/')) { return true; // Even on Windows: Potentially a path of a remote URL } #ifdef Q_OS_WIN return path.size() >= 2 && path.at(0).isLetter() && path.at(1) == ':'; #else return false; #endif } } QString KDevelop::toUrlOrLocalFile(const QUrl& url, QUrl::FormattingOptions options) { const auto str = url.toString(options | QUrl::PreferLocalFile); #ifdef Q_OS_WIN // potentially strip leading slash if (url.isLocalFile() && !str.isEmpty() && str[0] == '/') { return str.mid(1); // expensive copying, but we'd like toString(...) to properly format everything first } #endif return str; } Path::Path() { } Path::Path(const QString& pathOrUrl) -#if QT_VERSION >= 0x050400 : Path(QUrl::fromUserInput(pathOrUrl, QString(), QUrl::DefaultResolution)) -#else - : Path(QUrl::fromUserInput(pathOrUrl)) -#endif { } Path::Path(const QUrl& url) { if (!url.isValid()) { // empty or invalid Path return; } // we do not support urls with: // - fragments // - sub urls // - query // nor do we support relative urls if (url.hasFragment() || url.hasQuery() || url.isRelative() || url.path().isEmpty()) { // invalid qWarning("Path::init: invalid/unsupported Path encountered: \"%s\"", qPrintable(url.toDisplayString(QUrl::PreferLocalFile))); return; } if (!url.isLocalFile()) { // handle remote urls QString urlPrefix; urlPrefix += url.scheme(); urlPrefix += QLatin1String("://"); const QString user = url.userName(); if (!user.isEmpty()) { urlPrefix += user; urlPrefix += '@'; } urlPrefix += url.host(); if (url.port() != -1) { urlPrefix += ':' + QString::number(url.port()); } m_data << urlPrefix; } addPath(url.isLocalFile() ? url.toLocalFile() : url.path()); // support for root paths, they are valid but don't really contain any data if (m_data.isEmpty() || (isRemote() && m_data.size() == 1)) { m_data << QString(); } } Path::Path(const Path& other, const QString& child) : m_data(other.m_data) { if (isAbsolutePath(child)) { // absolute path: only share the remote part of @p other m_data.resize(isRemote() ? 1 : 0); } else if (!other.isValid() && !child.isEmpty()) { qWarning("Path::Path: tried to append relative path \"%s\" to invalid base", qPrintable(child)); return; } addPath(child); } static QString generatePathOrUrl(bool onlyPath, bool isLocalFile, const QVector& data) { // more or less a copy of QtPrivate::QStringList_join const int size = data.size(); if (size == 0) { return QString(); } int totalLength = 0; // separators: '/' totalLength += size; // skip Path segment if we only want the path int start = (onlyPath && !isLocalFile) ? 1 : 0; // path and url prefix for (int i = start; i < size; ++i) { totalLength += data.at(i).size(); } // build string representation QString res; res.reserve(totalLength); #ifdef Q_OS_WIN if (start == 0 && isLocalFile) { Q_ASSERT(data.at(0).endsWith(':')); // assume something along "C:" res += data.at(0); start++; } #endif for (int i = start; i < size; ++i) { if (i || isLocalFile) { res += '/'; } res += data.at(i); } return res; } QString Path::pathOrUrl() const { return generatePathOrUrl(false, isLocalFile(), m_data); } QString Path::path() const { return generatePathOrUrl(true, isLocalFile(), m_data); } QString Path::toLocalFile() const { return isLocalFile() ? path() : QString(); } QString Path::relativePath(const Path& path) const { if (!path.isValid()) { return QString(); } if (!isValid() || remotePrefix() != path.remotePrefix()) { // different remote destinations or we are invalid, return input as-is return path.pathOrUrl(); } // while I'd love to use QUrl::relativePath here, it seems to behave pretty // strangely, and adds unexpected "./" at the start for example // so instead, do it on our own based on _relativePath in kurl.cpp // this should also be more performant I think // Find where they meet int level = isRemote() ? 1 : 0; const int maxLevel = qMin(m_data.count(), path.m_data.count()); while(level < maxLevel && m_data.at(level) == path.m_data.at(level)) { ++level; } // Need to go down out of our path to the common branch. // but keep in mind that e.g. '/' paths have an empty name int backwardSegments = m_data.count() - level; if (backwardSegments && level < maxLevel && m_data.at(level).isEmpty()) { --backwardSegments; } // Now up up from the common branch to the second path. int forwardSegmentsLength = 0; for (int i = level; i < path.m_data.count(); ++i) { forwardSegmentsLength += path.m_data.at(i).length(); // slashes if (i + 1 != path.m_data.count()) { forwardSegmentsLength += 1; } } QString relativePath; relativePath.reserve((backwardSegments * 3) + forwardSegmentsLength); for(int i = 0; i < backwardSegments; ++i) { relativePath.append(QLatin1String("../")); } for (int i = level; i < path.m_data.count(); ++i) { relativePath.append(path.m_data.at(i)); if (i + 1 != path.m_data.count()) { relativePath.append(QLatin1Char('/')); } } Q_ASSERT(relativePath.length() == ((backwardSegments * 3) + forwardSegmentsLength)); return relativePath; } static bool isParentPath(const QVector& parent, const QVector& child, bool direct) { if (direct && child.size() != parent.size() + 1) { return false; } else if (!direct && child.size() <= parent.size()) { return false; } for (int i = 0; i < parent.size(); ++i) { if (child.at(i) != parent.at(i)) { // support for trailing '/' if (i + 1 == parent.size() && parent.at(i).isEmpty()) { return true; } // otherwise we take a different branch here return false; } } return true; } bool Path::isParentOf(const Path& path) const { if (!isValid() || !path.isValid() || remotePrefix() != path.remotePrefix()) { return false; } return isParentPath(m_data, path.m_data, false); } bool Path::isDirectParentOf(const Path& path) const { if (!isValid() || !path.isValid() || remotePrefix() != path.remotePrefix()) { return false; } return isParentPath(m_data, path.m_data, true); } QString Path::remotePrefix() const { return isRemote() ? m_data.first() : QString(); } bool Path::operator<(const Path& other) const { const int size = m_data.size(); const int otherSize = other.m_data.size(); const int toCompare = qMin(size, otherSize); // compare each Path segment in turn and try to return early for (int i = 0; i < toCompare; ++i) { int comparison = m_data.at(i).compare(other.m_data.at(i)); if (comparison == 0) { // equal, try next segment continue; } else { // return whether our segment is less then the other one return comparison < 0; } } // when we reach this point, all elements that we compared where equal // thus return whether we have less items than the other Path return size < otherSize; } QUrl Path::toUrl() const { return QUrl::fromUserInput(pathOrUrl()); } bool Path::isLocalFile() const { // if the first data element contains a '/' it is a Path prefix return !m_data.isEmpty() && !m_data.first().contains(QLatin1Char('/')); } bool Path::isRemote() const { return !m_data.isEmpty() && m_data.first().contains(QLatin1Char('/')); } QString Path::lastPathSegment() const { // remote Paths are offset by one, thus never return the first item of them as file name if (m_data.isEmpty() || (!isLocalFile() && m_data.size() == 1)) { return QString(); } return m_data.last(); } void Path::setLastPathSegment(const QString& name) { // remote Paths are offset by one, thus never return the first item of them as file name if (m_data.isEmpty() || (!isLocalFile() && m_data.size() == 1)) { // append the name to empty Paths or remote Paths only containing the Path prefix m_data.append(name); } else { // overwrite the last data member m_data.last() = name; } } static void cleanPath(QVector* data, const bool isRemote) { if (data->isEmpty()) { return; } const int startOffset = isRemote ? 1 : 0; const auto start = data->begin() + startOffset; auto it = start; while(it != data->end()) { if (*it == QLatin1String("..")) { if (it == start) { it = data->erase(it); } else { if (isWindowsDriveLetter(*(it - 1))) { it = data->erase(it); // keep the drive letter } else { it = data->erase(it - 1, it + 1); } } } else if (*it == QLatin1String(".")) { it = data->erase(it); } else { ++it; } } if (data->count() == startOffset) { data->append(QString()); } } // Optimized QString::split code for the specific Path use-case static QVarLengthArray splitPath(const QString &source) { QVarLengthArray list; int start = 0; int end = 0; while ((end = source.indexOf(QLatin1Char('/'), start)) != -1) { if (start != end) { list.append(source.mid(start, end - start)); } start = end + 1; } if (start != source.size()) { list.append(source.mid(start, -1)); } return list; } void Path::addPath(const QString& path) { if (path.isEmpty()) { return; } const auto& newData = splitPath(path); if (newData.isEmpty()) { if (m_data.size() == (isRemote() ? 1 : 0)) { // this represents the root path, we just turned an invalid path into it m_data << QString(); } return; } auto it = newData.begin(); if (!m_data.isEmpty() && m_data.last().isEmpty()) { // the root item is empty, set its contents and continue appending m_data.last() = *it; ++it; } std::copy(it, newData.end(), std::back_inserter(m_data)); cleanPath(&m_data, isRemote()); } Path Path::parent() const { if (m_data.isEmpty()) { return Path(); } Path ret(*this); if (m_data.size() == (1 + (isRemote() ? 1 : 0))) { // keep the root item, but clear it, otherwise we'd make the path invalid // or a URL a local path auto& root = ret.m_data.last(); if (!isWindowsDriveLetter(root)) { root.clear(); } } else { ret.m_data.pop_back(); } return ret; } bool Path::hasParent() const { const int rootIdx = isRemote() ? 1 : 0; return m_data.size() > rootIdx && !m_data[rootIdx].isEmpty(); } void Path::clear() { m_data.clear(); } Path Path::cd(const QString& dir) const { if (!isValid()) { return Path(); } return Path(*this, dir); } namespace KDevelop { uint qHash(const Path& path) { KDevHash hash; foreach (const QString& segment, path.segments()) { hash << qHash(segment); } return hash; } template static Path::List toPathList_impl(const Container& list) { Path::List ret; ret.reserve(list.size()); foreach (const auto& entry, list) { Path path(entry); if (path.isValid()) { ret << path; } } return ret; } Path::List toPathList(const QList& list) { return toPathList_impl(list); } Path::List toPathList(const QList< QString >& list) { return toPathList_impl(list); } } QDebug operator<<(QDebug s, const Path& string) { s.nospace() << string.pathOrUrl(); return s.space(); } namespace QTest { template<> char *toString(const Path &path) { return qstrdup(qPrintable(path.pathOrUrl())); } }