File indexing completed on 2024-04-28 04:37:16

0001 /*
0002     SPDX-FileCopyrightText: 2002 Matthias Hoelzer-Kluepfel <hoelzer@kde.org>
0003     SPDX-FileCopyrightText: 2002 Bernd Gehrmann <bernd@kdevelop.org>
0004     SPDX-FileCopyrightText: 2003 Roberto Raggi <roberto@kdevelop.org>
0005     SPDX-FileCopyrightText: 2003-2008 Hamish Rodda <rodda@kde.org>
0006     SPDX-FileCopyrightText: 2003 Harald Fernengel <harry@kdevelop.org>
0007     SPDX-FileCopyrightText: 2003 Jens Dagerbo <jens.dagerbo@swipnet.se>
0008     SPDX-FileCopyrightText: 2005 Adam Treat <treat@kde.org>
0009     SPDX-FileCopyrightText: 2004-2007 Alexander Dymo <adymo@kdevelop.org>
0010     SPDX-FileCopyrightText: 2007 Andreas Pakulat <apaku@gmx.de>
0011 
0012     SPDX-License-Identifier: LGPL-2.0-or-later
0013 */
0014 
0015 #include "documentcontroller.h"
0016 
0017 #include <QApplication>
0018 #include <QDBusConnection>
0019 #include <QFileInfo>
0020 #include <QMimeDatabase>
0021 #include <QRegularExpression>
0022 #include <QPointer>
0023 
0024 #include <KActionCollection>
0025 #include <KEncodingFileDialog>
0026 #include <KIO/StatJob>
0027 #include <KJobWidgets>
0028 #include <KLocalizedString>
0029 #include <KMessageBox>
0030 #include <KMessageBox_KDevCompat>
0031 #include <KProtocolInfo>
0032 #include <KRecentFilesAction>
0033 #include <KTextEditor/Document>
0034 #include <KTextEditor/View>
0035 #include <KTextEditor/AnnotationInterface>
0036 
0037 #include <sublime/area.h>
0038 #include <sublime/message.h>
0039 #include <sublime/view.h>
0040 #include <interfaces/iplugincontroller.h>
0041 #include <interfaces/iprojectcontroller.h>
0042 #include <interfaces/ibuddydocumentfinder.h>
0043 #include <interfaces/iproject.h>
0044 #include <interfaces/iselectioncontroller.h>
0045 #include <interfaces/context.h>
0046 #include <project/projectmodel.h>
0047 #include <util/scopeddialog.h>
0048 #include <util/path.h>
0049 
0050 #include "core.h"
0051 #include "mainwindow.h"
0052 #include "textdocument.h"
0053 #include "uicontroller.h"
0054 #include "partcontroller.h"
0055 #include "savedialog.h"
0056 #include "debug.h"
0057 
0058 #include <vcs/interfaces/ibasicversioncontrol.h>
0059 #include <vcs/vcspluginhelper.h>
0060 
0061 #include <algorithm>
0062 
0063 #define EMPTY_DOCUMENT_URL i18n("Untitled")
0064 
0065 using namespace KDevelop;
0066 
0067 
0068 class KDevelop::DocumentControllerPrivate
0069 {
0070 public:
0071     struct OpenFileResult
0072     {
0073         QList<QUrl> urls;
0074         QString encoding;
0075     };
0076 
0077     explicit DocumentControllerPrivate(DocumentController* c)
0078         : controller(c)
0079         , fileOpenRecent(nullptr)
0080     {
0081     }
0082 
0083     ~DocumentControllerPrivate() = default;
0084 
0085     // used to map urls to open docs
0086     QHash< QUrl, IDocument* > documents;
0087     bool shuttingDown = false;
0088 
0089     QHash< QString, IDocumentFactory* > factories;
0090 
0091     struct HistoryEntry
0092     {
0093         HistoryEntry() {}
0094         HistoryEntry( const QUrl & u, const KTextEditor::Cursor& cursor );
0095 
0096         QUrl url;
0097         KTextEditor::Cursor cursor;
0098         int id;
0099     };
0100 
0101     void removeDocument(Sublime::Document *doc)
0102     {
0103         const QList<QUrl> urlsForDoc = documents.keys(qobject_cast<KDevelop::IDocument*>(doc));
0104         for (const QUrl& url : urlsForDoc) {
0105             qCDebug(SHELL) << "destroying document" << doc;
0106             documents.remove(url);
0107         }
0108     }
0109 
0110     OpenFileResult showOpenFile() const
0111     {
0112         QUrl dir;
0113         if ( controller->activeDocument() ) {
0114             dir = controller->activeDocument()->url().adjusted(QUrl::RemoveFilename);
0115         } else  {
0116             const auto cfg = KSharedConfig::openConfig()->group("Open File");
0117             dir = cfg.readEntry( "Last Open File Directory", Core::self()->projectController()->projectsBaseDirectory() );
0118         }
0119 
0120         const auto caption = i18nc("@title:window", "Open File");
0121         const auto filter = i18n("*|Text File\n");
0122         auto parent = Core::self()->uiControllerInternal()->defaultMainWindow();
0123 
0124         // use special dialogs in a KDE session, native dialogs elsewhere
0125         if (qEnvironmentVariableIsSet("KDE_FULL_SESSION")) {
0126             const auto result = KEncodingFileDialog::getOpenUrlsAndEncoding(QString(), dir,
0127                 filter, parent, caption);
0128             return {result.URLs, result.encoding};
0129         }
0130 
0131         // note: can't just filter on text files using the native dialog, just display all files
0132         // see https://phabricator.kde.org/D622#11679
0133         const auto urls = QFileDialog::getOpenFileUrls(parent, caption, dir);
0134         return {urls, QString()};
0135     }
0136 
0137     void chooseDocument()
0138     {
0139         const auto res = showOpenFile();
0140         if( !res.urls.isEmpty() ) {
0141             QString encoding = res.encoding;
0142             for (const QUrl& u : res.urls) {
0143                 openDocumentInternal(u, QString(), KTextEditor::Range::invalid(), encoding  );
0144             }
0145         }
0146 
0147     }
0148 
0149     void changeDocumentUrl(KDevelop::IDocument* document, const QUrl& previousUrl)
0150     {
0151         const auto it = documents.constFind(previousUrl);
0152         if (it == documents.cend()) {
0153             qCWarning(SHELL) << "a renamed document is not registered:" << document << previousUrl.toString()
0154                              << document->url().toString();
0155             return;
0156         }
0157         Q_ASSERT(it.value() == document);
0158 
0159         const auto documentIt = documents.constFind(document->url());
0160         if (documentIt != documents.constEnd()) {
0161             // Weird situation (saving as a file that is already open)
0162             IDocument* origDoc = *documentIt;
0163             Q_ASSERT_X(origDoc != document, Q_FUNC_INFO, "Duplicate documentUrlChanged signal emission?");
0164             if (origDoc->state() & IDocument::Modified) {
0165                 // given that the file has been saved, close the saved file as the other instance will become conflicted on disk
0166                 document->close(); // this closing erases the iterator `it`
0167                 controller->activateDocument( origDoc );
0168                 return;
0169             }
0170             // Otherwise close the original document, but first erase the iterator `it`,
0171             // because the closing erases documentIt, which can invalidate `it`.
0172             documents.erase(it);
0173             origDoc->close();
0174         } else {
0175             documents.erase(it); // erase the previous-URL entry
0176         }
0177 
0178         documents.insert(document->url(), document);
0179 
0180         if (!controller->isEmptyDocumentUrl(document->url()))
0181         {
0182             fileOpenRecent->addUrl(document->url());
0183         }
0184     }
0185 
0186     KDevelop::IDocument* findBuddyDocument(const QUrl &url, IBuddyDocumentFinder* finder)
0187     {
0188         const QList<KDevelop::IDocument*> allDocs = controller->openDocuments();
0189         for (KDevelop::IDocument* doc : allDocs) {
0190             if(finder->areBuddies(url, doc->url())) {
0191                 return doc;
0192             }
0193         }
0194         return nullptr;
0195     }
0196 
0197     static bool fileExists(const QUrl& url)
0198     {
0199         if (url.isLocalFile()) {
0200             return QFile::exists(url.toLocalFile());
0201         } else {
0202             auto job = KIO::statDetails(url, KIO::StatJob::SourceSide, KIO::StatNoDetails, KIO::HideProgressInfo);
0203             KJobWidgets::setWindow(job, ICore::self()->uiController()->activeMainWindow());
0204             return job->exec();
0205         }
0206     };
0207 
0208     IDocument* openDocumentInternal( const QUrl & inputUrl, const QString& prefName = QString(),
0209         const KTextEditor::Range& range = KTextEditor::Range::invalid(), const QString& encoding = QString(),
0210         DocumentController::DocumentActivationParams activationParams = {},
0211         IDocument* buddy = nullptr)
0212     {
0213         Q_ASSERT(!inputUrl.isRelative());
0214         Q_ASSERT(!inputUrl.fileName().isEmpty() || !inputUrl.isLocalFile());
0215         QString _encoding = encoding;
0216 
0217         QUrl url = inputUrl;
0218 
0219         if ( url.isEmpty() && (!activationParams.testFlag(IDocumentController::DoNotCreateView)) )
0220         {
0221             const auto res = showOpenFile();
0222             if( !res.urls.isEmpty() )
0223                 url = res.urls.first();
0224             _encoding = res.encoding;
0225             if ( url.isEmpty() )
0226                 //still no url
0227                 return nullptr;
0228         }
0229 
0230         KSharedConfig::openConfig()->group("Open File").writeEntry( "Last Open File Directory", url.adjusted(QUrl::RemoveFilename) );
0231 
0232         // clean it and resolve possible symlink
0233         url = url.adjusted( QUrl::NormalizePathSegments );
0234         if ( url.isLocalFile() )
0235         {
0236             QString path = QFileInfo( url.toLocalFile() ).canonicalFilePath();
0237             if ( !path.isEmpty() )
0238                 url = QUrl::fromLocalFile( path );
0239         }
0240 
0241         //get a part document
0242         IDocument* doc = documents.value(url);
0243         if (!doc)
0244         {
0245             QMimeType mimeType;
0246 
0247             if (DocumentController::isEmptyDocumentUrl(url))
0248             {
0249                 mimeType = QMimeDatabase().mimeTypeForName(QStringLiteral("text/plain"));
0250             }
0251             else if (!url.isValid())
0252             {
0253                 // Exit if the url is invalid (should not happen)
0254                 // If the url is valid and the file does not already exist,
0255                 // kate creates the file and gives a message saying so
0256                 qCDebug(SHELL) << "invalid URL:" << url.url();
0257                 return nullptr;
0258             }
0259             else if (KProtocolInfo::isKnownProtocol(url.scheme()) && !fileExists(url))
0260             {
0261                 //Don't create a new file if we are not in the code mode.
0262                 if (ICore::self()->uiController()->activeArea()->objectName() != QLatin1String("code")) {
0263                     return nullptr;
0264                 }
0265                 // enfore text mime type in order to create a kate part editor which then can be used to create the file
0266                 // otherwise we could end up opening e.g. okteta which then crashes, see: https://bugs.kde.org/id=326434
0267                 mimeType = QMimeDatabase().mimeTypeForName(QStringLiteral("text/plain"));
0268             }
0269             else
0270             {
0271                 mimeType = QMimeDatabase().mimeTypeForUrl(url);
0272 
0273                 if(!url.isLocalFile() && mimeType.isDefault())
0274                 {
0275                     // fall back to text/plain, for remote files without extension, i.e. COPYING, LICENSE, ...
0276                     // using a synchronous KIO::MimetypeJob is hazardous and may lead to repeated calls to
0277                     // this function without it having returned in the first place
0278                     // and this function is *not* reentrant, see assert below:
0279                     // Q_ASSERT(!documents.contains(url) || documents[url]==doc);
0280                     mimeType = QMimeDatabase().mimeTypeForName(QStringLiteral("text/plain"));
0281                 }
0282             }
0283 
0284             // is the URL pointing to a directory?
0285             if (mimeType.inherits(QStringLiteral("inode/directory")))
0286             {
0287                 qCDebug(SHELL) << "cannot open directory:" << url.url();
0288                 return nullptr;
0289             }
0290 
0291             if( prefName.isEmpty() )
0292             {
0293                 // Try to find a plugin that handles this mimetype
0294                 QVariantMap constraints;
0295                 constraints.insert(QStringLiteral("X-KDevelop-SupportedMimeTypes"), mimeType.name());
0296                 Core::self()->pluginController()->pluginForExtension(QString(), QString(), constraints);
0297             }
0298 
0299             if( IDocumentFactory* factory = factories.value(mimeType.name()))
0300             {
0301                 doc = factory->create(url, Core::self());
0302             }
0303 
0304             if(!doc) {
0305                 if( !prefName.isEmpty() )
0306                 {
0307                     doc = new PartDocument(url, Core::self(), prefName);
0308                 } else  if ( Core::self()->partControllerInternal()->isTextType(mimeType))
0309                 {
0310                     doc = new TextDocument(url, Core::self(), _encoding);
0311                 } else if( Core::self()->partControllerInternal()->canCreatePart(url) )
0312                 {
0313                     doc = new PartDocument(url, Core::self());
0314                 } else
0315                 {
0316                     int openAsText = KMessageBox::questionTwoActions(
0317                         nullptr,
0318                         i18n("KDevelop could not find the editor for file '%1' of type %2.\nDo you want to open it as "
0319                              "plain text?",
0320                              url.fileName(), mimeType.name()),
0321                         i18nc("@title:window", "Could Not Find Editor"),
0322                         KGuiItem(i18nc("@action:button", "Open as Plain Text"), QStringLiteral("text-plaim")),
0323                         KGuiItem(i18nc("@action:button", "Do Not Open"), QStringLiteral("dialog-cancel")),
0324                         QStringLiteral("AskOpenWithTextEditor"));
0325                     if (openAsText == KMessageBox::PrimaryAction)
0326                         doc = new TextDocument(url, Core::self(), _encoding);
0327                     else
0328                         return nullptr;
0329                 }
0330             }
0331         }
0332 
0333         // The url in the document must equal the current url, else the housekeeping will get broken
0334         Q_ASSERT(!doc || doc->url() == url);
0335 
0336         if(doc && openDocumentInternal(doc, range, activationParams, buddy))
0337             return doc;
0338         else
0339             return nullptr;
0340 
0341     }
0342 
0343     bool openDocumentInternal(IDocument* doc,
0344                                 const KTextEditor::Range& range,
0345                                 DocumentController::DocumentActivationParams activationParams,
0346                                 IDocument* buddy = nullptr)
0347     {
0348         IDocument* previousActiveDocument = controller->activeDocument();
0349         KTextEditor::View* previousActiveTextView = ICore::self()->documentController()->activeTextDocumentView();
0350         KTextEditor::Cursor previousActivePosition;
0351         if(previousActiveTextView)
0352             previousActivePosition = previousActiveTextView->cursorPosition();
0353 
0354         QUrl url=doc->url();
0355         UiController *uiController = Core::self()->uiControllerInternal();
0356         Sublime::Area *area = uiController->activeArea();
0357 
0358         //We can't have the same url in many documents
0359         //so we check it's already the same if it exists
0360         //contains=>it's the same
0361         Q_ASSERT(!documents.contains(url) || documents[url]==doc);
0362 
0363         auto *sdoc = dynamic_cast<Sublime::Document*>(doc);
0364         if( !sdoc )
0365         {
0366             documents.remove(url);
0367             delete doc;
0368             return false;
0369         }
0370 
0371         //We check if it was already opened before
0372         const bool wasClosed = !documents.contains(url);
0373         if (wasClosed) {
0374             documents[url]=doc;
0375 
0376             // react on document deletion - we need to clean up controller structures
0377             QObject::connect(sdoc, &Sublime::Document::aboutToDelete, controller,
0378                              &DocumentController::notifyDocumentClosed);
0379         }
0380 
0381         if (!activationParams.testFlag(IDocumentController::DoNotCreateView))
0382         {
0383             //find a view if there's one already opened in this area
0384             Sublime::AreaIndex* activeViewIdx = area->indexOf(uiController->activeSublimeWindow()->activeView());
0385             const auto& views = sdoc->views();
0386             auto it = std::find_if(views.begin(), views.end(), [&](Sublime::View* view) {
0387                 Sublime::AreaIndex* areaIdx = area->indexOf(view);
0388                 return (areaIdx && areaIdx == activeViewIdx);
0389             });
0390             Sublime::View* partView = (it != views.end()) ? *it : nullptr;
0391             bool addView = false;
0392             if (!partView)
0393             {
0394                 //no view currently shown for this url
0395                 partView = sdoc->createView();
0396                 addView = true;
0397             }
0398 
0399             if(addView) {
0400                 // This code is never executed when restoring session on startup,
0401                 // only when opening a file manually
0402 
0403                 Sublime::View* buddyView = nullptr;
0404                 bool placeAfterBuddy = true;
0405                 if(Core::self()->uiControllerInternal()->arrangeBuddies() && !buddy && doc->mimeType().isValid()) {
0406                     // If buddy is not set, look for a (usually) plugin which handles this URL's mimetype
0407                     // and use its IBuddyDocumentFinder, if available, to find a buddy document
0408                     QString mime = doc->mimeType().name();
0409                     IBuddyDocumentFinder* buddyFinder = IBuddyDocumentFinder::finderForMimeType(mime);
0410                     if(buddyFinder) {
0411                         buddy = findBuddyDocument(url, buddyFinder);
0412                         if(buddy) {
0413                             placeAfterBuddy = buddyFinder->buddyOrder(buddy->url(), doc->url());
0414                         }
0415                     }
0416                 }
0417 
0418                 if(buddy) {
0419                     auto* sublimeDocBuddy = dynamic_cast<Sublime::Document*>(buddy);
0420 
0421                     if(sublimeDocBuddy) {
0422                         Sublime::AreaIndex *pActiveViewIndex = area->indexOf(uiController->activeSublimeWindow()->activeView());
0423                         if(pActiveViewIndex) {
0424                             // try to find existing View of buddy document in current active view's tab
0425                             const auto& activeAreaViews = pActiveViewIndex->views();
0426                             const auto& buddyViews = sublimeDocBuddy->views();
0427                             auto it = std::find_if(activeAreaViews.begin(), activeAreaViews.end(), [&](Sublime::View* view) {
0428                                 return buddyViews.contains(view);
0429                             });
0430                             if (it != activeAreaViews.end()) {
0431                                 buddyView = *it;
0432                             }
0433                         }
0434                     }
0435                 }
0436 
0437                 // add view to the area
0438                 if(buddyView && area->indexOf(buddyView)) {
0439                     if(placeAfterBuddy) {
0440                         // Adding new view after buddy view, simple case
0441                         area->addView(partView, area->indexOf(buddyView), buddyView);
0442                     }
0443                     else {
0444                         // First new view, then buddy view
0445                         area->addView(partView, area->indexOf(buddyView), buddyView);
0446                         // move buddyView tab after the new document
0447                         area->removeView(buddyView);
0448                         area->addView(buddyView, area->indexOf(partView), partView);
0449                     }
0450                 }
0451                 else {
0452                     // no buddy found for new document / plugin does not support buddies / buddy feature disabled
0453                     Sublime::View *activeView = uiController->activeSublimeWindow()->activeView();
0454                     Sublime::UrlDocument *activeDoc = nullptr;
0455                     IBuddyDocumentFinder *buddyFinder = nullptr;
0456                     if(activeView)
0457                         activeDoc = qobject_cast<Sublime::UrlDocument *>(activeView->document());
0458                     if(activeDoc && Core::self()->uiControllerInternal()->arrangeBuddies()) {
0459                         QString mime = QMimeDatabase().mimeTypeForUrl(activeDoc->url()).name();
0460                         buddyFinder = IBuddyDocumentFinder::finderForMimeType(mime);
0461                     }
0462 
0463                     if(Core::self()->uiControllerInternal()->openAfterCurrent() &&
0464                        Core::self()->uiControllerInternal()->arrangeBuddies() &&
0465                        buddyFinder)
0466                     {
0467                         // Check if active document's buddy is directly next to it.
0468                         // For example, we have the already-open tabs | *foo.h* | foo.cpp | , foo.h is active.
0469                         // When we open a new document here (and the buddy feature is enabled),
0470                         // we do not want to separate foo.h and foo.cpp, so we take care and avoid this.
0471                         Sublime::AreaIndex *activeAreaIndex = area->indexOf(activeView);
0472                         int pos = activeAreaIndex->views().indexOf(activeView);
0473                         Sublime::View *afterActiveView = activeAreaIndex->views().value(pos+1, nullptr);
0474 
0475                         Sublime::UrlDocument *activeDoc = nullptr, *afterActiveDoc = nullptr;
0476                         if(activeView && afterActiveView) {
0477                             activeDoc = qobject_cast<Sublime::UrlDocument *>(activeView->document());
0478                             afterActiveDoc = qobject_cast<Sublime::UrlDocument *>(afterActiveView->document());
0479                         }
0480                         if(activeDoc && afterActiveDoc &&
0481                            buddyFinder->areBuddies(activeDoc->url(), afterActiveDoc->url()))
0482                         {
0483                             // don't insert in between of two buddies, but after them
0484                             area->addView(partView, activeAreaIndex, afterActiveView);
0485                         }
0486                         else {
0487                             // The active document's buddy is not directly after it
0488                             // => no problem, insert after active document
0489                             area->addView(partView, activeView);
0490                         }
0491                     }
0492                     else {
0493                         // Opening as last tab won't disturb our buddies
0494                         // Same, if buddies are disabled, we needn't care about them.
0495 
0496                         // this method places the tab according to openAfterCurrent()
0497                         area->addView(partView, activeView);
0498                     }
0499                 }
0500             }
0501 
0502             if (!activationParams.testFlag(IDocumentController::DoNotActivate))
0503             {
0504                 uiController->activeSublimeWindow()->activateView(
0505                     partView, !activationParams.testFlag(IDocumentController::DoNotFocus));
0506             }
0507             if (!activationParams.testFlag(IDocumentController::DoNotAddToRecentOpen) && !controller->isEmptyDocumentUrl(url))
0508             {
0509                 fileOpenRecent->addUrl( url );
0510             }
0511 
0512             if( range.isValid() )
0513             {
0514                 if (range.isEmpty())
0515                     doc->setCursorPosition( range.start() );
0516                 else
0517                     doc->setTextSelection( range );
0518             }
0519         }
0520 
0521         // Deferred signals, wait until it's all ready first
0522         if (wasClosed) {
0523             emit controller->documentOpened( doc );
0524         }
0525 
0526         if (!activationParams.testFlag(IDocumentController::DoNotActivate) && doc != controller->activeDocument())
0527             emit controller->documentActivated( doc );
0528 
0529         saveAll->setEnabled(true);
0530         revertAll->setEnabled(true);
0531         close->setEnabled(true);
0532         closeAll->setEnabled(true);
0533         closeAllOthers->setEnabled(true);
0534 
0535         KTextEditor::Cursor activePosition;
0536         if(range.isValid())
0537             activePosition = range.start();
0538         else if(KTextEditor::View* v = doc->activeTextView())
0539             activePosition = v->cursorPosition();
0540 
0541         if (doc != previousActiveDocument || activePosition != previousActivePosition)
0542             emit controller->documentJumpPerformed(doc, activePosition, previousActiveDocument, previousActivePosition);
0543 
0544         return true;
0545     }
0546 
0547     DocumentController* const controller;
0548 
0549     QPointer<QAction> saveAll;
0550     QPointer<QAction> revertAll;
0551     QPointer<QAction> close;
0552     QPointer<QAction> closeAll;
0553     QPointer<QAction> closeAllOthers;
0554     KRecentFilesAction* fileOpenRecent;
0555 };
0556 Q_DECLARE_TYPEINFO(KDevelop::DocumentControllerPrivate::HistoryEntry, Q_MOVABLE_TYPE);
0557 
0558 DocumentController::DocumentController( QObject *parent )
0559         : IDocumentController( parent )
0560         , d_ptr(new DocumentControllerPrivate(this))
0561 {
0562     setObjectName(QStringLiteral("DocumentController"));
0563     QDBusConnection::sessionBus().registerObject( QStringLiteral("/org/kdevelop/DocumentController"),
0564         this, QDBusConnection::ExportScriptableSlots );
0565 
0566     connect(this, &DocumentController::documentUrlChanged, this, [this](IDocument* document, const QUrl& previousUrl) {
0567         Q_D(DocumentController);
0568         d->changeDocumentUrl(document, previousUrl);
0569     });
0570 
0571     if(!(Core::self()->setupFlags() & Core::NoUi)) setupActions();
0572 }
0573 
0574 void DocumentController::initialize()
0575 {
0576     Q_D(DocumentController);
0577 
0578     d->shuttingDown = false; // required by test_documentcontroller
0579 }
0580 
0581 void DocumentController::cleanup()
0582 {
0583     Q_D(DocumentController);
0584 
0585     d->shuttingDown = true;
0586 
0587     if (d->fileOpenRecent)
0588         d->fileOpenRecent->saveEntries( KConfigGroup(KSharedConfig::openConfig(), "Recent Files" ) );
0589 
0590     // Close all documents without checking if they should be saved.
0591     // This is because the user gets a chance to save them during MainWindow::queryClose.
0592     const auto documents = openDocuments();
0593     for (IDocument* doc : documents) {
0594         doc->close(IDocument::Discard);
0595     }
0596 }
0597 
0598 DocumentController::~DocumentController() = default;
0599 
0600 void DocumentController::setupActions()
0601 {
0602     Q_D(DocumentController);
0603 
0604     KActionCollection* ac = Core::self()->uiControllerInternal()->defaultMainWindow()->actionCollection();
0605 
0606     QAction* action;
0607 
0608     action = ac->addAction( QStringLiteral("file_open") );
0609     action->setIcon(QIcon::fromTheme(QStringLiteral("document-open")));
0610     ac->setDefaultShortcut(action, Qt::CTRL | Qt::Key_O);
0611     action->setText(i18nc("@action",  "&Open..." ) );
0612     connect(action, &QAction::triggered,
0613             this, [this] { Q_D(DocumentController); d->chooseDocument(); } );
0614     action->setToolTip( i18nc("@info:tooltip", "Open file" ) );
0615     action->setWhatsThis( i18nc("@info:whatsthis", "Opens a file for editing." ) );
0616 
0617     d->fileOpenRecent = KStandardAction::openRecent(this,
0618                     SLOT(slotOpenDocument(QUrl)), ac);
0619     d->fileOpenRecent->setWhatsThis(i18nc("@info:whatsthis", "This lists files which you have opened recently, and allows you to easily open them again."));
0620     d->fileOpenRecent->loadEntries( KConfigGroup(KSharedConfig::openConfig(), "Recent Files" ) );
0621 
0622     action = d->saveAll = ac->addAction( QStringLiteral("file_save_all") );
0623     action->setIcon(QIcon::fromTheme(QStringLiteral("document-save")));
0624     action->setText(i18nc("@action", "Save Al&l" ) );
0625     connect( action, &QAction::triggered, this, &DocumentController::slotSaveAllDocuments );
0626     action->setToolTip( i18nc("@info:tooltip", "Save all open documents" ) );
0627     action->setWhatsThis( i18nc("@info:whatsthis", "Save all open documents, prompting for additional information when necessary." ) );
0628     ac->setDefaultShortcut(action, QKeySequence(Qt::CTRL | Qt::Key_L));
0629     action->setEnabled(false);
0630 
0631     action = d->revertAll = ac->addAction( QStringLiteral("file_revert_all") );
0632     action->setIcon(QIcon::fromTheme(QStringLiteral("document-revert")));
0633     action->setText(i18nc("@action", "Reload All" ) );
0634     connect( action, &QAction::triggered, this, &DocumentController::reloadAllDocuments );
0635     action->setToolTip( i18nc("@info:tooltip", "Revert all open documents" ) );
0636     action->setWhatsThis( i18nc("@info:whatsthis", "Revert all open documents, returning to the previously saved state." ) );
0637     action->setEnabled(false);
0638 
0639     action = d->close = ac->addAction( QStringLiteral("file_close") );
0640     action->setIcon(QIcon::fromTheme(QStringLiteral("document-close")));
0641     ac->setDefaultShortcut(action, Qt::CTRL | Qt::Key_W);
0642     action->setText( i18nc("@action", "&Close" ) );
0643     connect( action, &QAction::triggered, this, &DocumentController::fileClose );
0644     action->setToolTip( i18nc("@info:tooltip", "Close file" ) );
0645     action->setWhatsThis( i18nc("@info:whatsthis", "Closes current file." ) );
0646     action->setEnabled(false);
0647 
0648     action = d->closeAll = ac->addAction( QStringLiteral("file_close_all") );
0649     action->setIcon(QIcon::fromTheme(QStringLiteral("document-close")));
0650     action->setText(i18nc("@action", "Clos&e All" ) );
0651     connect( action, &QAction::triggered, this, &DocumentController::closeAllDocuments );
0652     action->setToolTip( i18nc("@info:tooltip", "Close all open documents" ) );
0653     action->setWhatsThis( i18nc("@info:whatsthis", "Close all open documents, prompting for additional information when necessary." ) );
0654     action->setEnabled(false);
0655 
0656     action = d->closeAllOthers = ac->addAction( QStringLiteral("file_closeother") );
0657     action->setIcon(QIcon::fromTheme(QStringLiteral("document-close")));
0658     ac->setDefaultShortcut(action, Qt::CTRL | Qt::SHIFT | Qt::Key_W);
0659     action->setText(i18nc("@action", "Close All Ot&hers" ) );
0660     connect( action, &QAction::triggered, this, &DocumentController::closeAllOtherDocuments );
0661     action->setToolTip( i18nc("@info:tooltip", "Close all other documents" ) );
0662     action->setWhatsThis( i18nc("@info:whatsthis", "Close all open documents, with the exception of the currently active document." ) );
0663     action->setEnabled(false);
0664 
0665     action = ac->addAction( QStringLiteral("vcsannotate_current_document") );
0666     connect( action, &QAction::triggered, this, &DocumentController::vcsAnnotateCurrentDocument );
0667     action->setText( i18nc("@action", "Show Annotate on Current Document") );
0668     action->setIconText( i18nc("@action", "Annotate" ) );
0669     action->setIcon( QIcon::fromTheme(QStringLiteral("user-properties")) );
0670 }
0671 
0672 void DocumentController::slotOpenDocument(const QUrl &url)
0673 {
0674     openDocument(url);
0675 }
0676 
0677 IDocument* DocumentController::openDocumentFromText( const QString& data )
0678 {
0679     IDocument* d = openDocument(nextEmptyDocumentUrl());
0680     Q_ASSERT(d->textDocument());
0681     d->textDocument()->setText( data );
0682     return d;
0683 }
0684 
0685 bool DocumentController::openDocumentFromTextSimple( QString text )
0686 {
0687     return (bool)openDocumentFromText( text );
0688 }
0689 
0690 bool DocumentController::openDocumentSimple( QString url, int line, int column )
0691 {
0692     return (bool)openDocument( QUrl::fromUserInput(url), KTextEditor::Cursor( line, column ) );
0693 }
0694 
0695 IDocument* DocumentController::openDocument( const QUrl& inputUrl, const QString& prefName )
0696 {
0697     Q_D(DocumentController);
0698 
0699     return d->openDocumentInternal( inputUrl, prefName );
0700 }
0701 
0702 IDocument* DocumentController::openDocument( const QUrl & inputUrl,
0703         const KTextEditor::Range& range,
0704         DocumentActivationParams activationParams,
0705         const QString& encoding, IDocument* buddy)
0706 {
0707     Q_D(DocumentController);
0708 
0709     if (d->shuttingDown) {
0710         // When a user exits KDevelop during debugging, a code breakpoint can be hit,
0711         // and as a consequence DebugController::showStepInSource() be called
0712         // in the event loop started by Core::cleanup() => BackgroundParser::waitForIdle().
0713         // Oblivious to the application state, DebugController then tries to open a document,
0714         // which eventually results in a crash inside a slot connected to either
0715         // &IDocumentController::textDocumentCreated or &IDocumentController::documentLoaded
0716         // (these signals are emitted in the process of opening a document).
0717         // Even had there been no crash, we should not open documents after cleanup(),
0718         // because we will never close them.
0719         qCDebug(SHELL) << "refusing to open document" << inputUrl << "after cleanup()";
0720         return nullptr;
0721     }
0722 
0723     return d->openDocumentInternal(inputUrl, QString(), range, encoding, activationParams, buddy);
0724 }
0725 
0726 
0727 bool DocumentController::openDocument(IDocument* doc,
0728                                       const KTextEditor::Range& range,
0729                                       DocumentActivationParams activationParams,
0730                                       IDocument* buddy)
0731 {
0732     Q_D(DocumentController);
0733 
0734     return d->openDocumentInternal( doc, range, activationParams, buddy);
0735 }
0736 
0737 
0738 void DocumentController::fileClose()
0739 {
0740     IDocument *activeDoc = activeDocument();
0741     if (activeDoc)
0742     {
0743         UiController *uiController = Core::self()->uiControllerInternal();
0744         Sublime::View *activeView = uiController->activeSublimeWindow()->activeView();
0745 
0746         uiController->activeArea()->closeView(activeView);
0747     }
0748 }
0749 
0750 bool DocumentController::closeDocument( const QUrl &url )
0751 {
0752     Q_D(DocumentController);
0753 
0754     const auto documentIt = d->documents.constFind(url);
0755     if (documentIt == d->documents.constEnd())
0756         return false;
0757 
0758     //this will remove all views and after the last view is removed, the
0759     //document will be self-destructed and removeDocument() slot will catch that
0760     //and clean up internal data structures
0761     (*documentIt)->close();
0762     return true;
0763 }
0764 
0765 void DocumentController::notifyDocumentClosed(Sublime::Document* doc_)
0766 {
0767     Q_D(DocumentController);
0768 
0769     auto* doc = qobject_cast<IDocument*>(doc_);
0770     Q_ASSERT(doc);
0771 
0772     d->removeDocument(doc_);
0773 
0774     if (d->documents.isEmpty()) {
0775         if (d->saveAll)
0776             d->saveAll->setEnabled(false);
0777         if (d->revertAll)
0778             d->revertAll->setEnabled(false);
0779         if (d->close)
0780             d->close->setEnabled(false);
0781         if (d->closeAll)
0782             d->closeAll->setEnabled(false);
0783         if (d->closeAllOthers)
0784             d->closeAllOthers->setEnabled(false);
0785     }
0786 
0787     emit documentClosed(doc);
0788 }
0789 
0790 IDocument * DocumentController::documentForUrl( const QUrl & dirtyUrl ) const
0791 {
0792     Q_D(const DocumentController);
0793 
0794     if (dirtyUrl.isEmpty()) {
0795         return nullptr;
0796     }
0797     Q_ASSERT(!dirtyUrl.isRelative());
0798     Q_ASSERT(!dirtyUrl.fileName().isEmpty() || !dirtyUrl.isLocalFile());
0799     //Fix urls that might not be normalized
0800     return d->documents.value( dirtyUrl.adjusted( QUrl::NormalizePathSegments ), nullptr );
0801 }
0802 
0803 QList<IDocument*> DocumentController::openDocuments() const
0804 {
0805     Q_D(const DocumentController);
0806 
0807     QList<IDocument*> opened;
0808     for (IDocument* doc : qAsConst(d->documents)) {
0809         auto *sdoc = dynamic_cast<Sublime::Document*>(doc);
0810         if( !sdoc )
0811         {
0812             continue;
0813         }
0814         if (!sdoc->views().isEmpty())
0815             opened << doc;
0816     }
0817     return opened;
0818 }
0819 
0820 void DocumentController::activateDocument( IDocument * document, const KTextEditor::Range& range )
0821 {
0822     // TODO avoid some code in openDocument?
0823     Q_ASSERT(document);
0824     openDocument(document->url(), range, IDocumentController::DoNotAddToRecentOpen);
0825 }
0826 
0827 void DocumentController::slotSaveAllDocuments()
0828 {
0829     saveAllDocuments(IDocument::Silent);
0830 }
0831 
0832 bool DocumentController::saveAllDocuments(IDocument::DocumentSaveMode mode)
0833 {
0834     return saveSomeDocuments(openDocuments(), mode);
0835 }
0836 
0837 bool KDevelop::DocumentController::saveSomeDocuments(const QList< IDocument * > & list, IDocument::DocumentSaveMode mode)
0838 {
0839     if (mode & IDocument::Silent) {
0840         const auto documents = modifiedDocuments(list);
0841         for (IDocument* doc : documents) {
0842             if( !DocumentController::isEmptyDocumentUrl(doc->url()) && !doc->save(mode) )
0843             {
0844                 if( doc )
0845                     qCWarning(SHELL) << "!! Could not save document:" << doc->url();
0846                 else
0847                     qCWarning(SHELL) << "!! Could not save document as its NULL";
0848             }
0849             // TODO if (!ret) showErrorDialog() ?
0850         }
0851 
0852     } else {
0853         // Ask the user which documents to save
0854         QList<IDocument*> checkSave = modifiedDocuments(list);
0855 
0856         if (!checkSave.isEmpty()) {
0857             ScopedDialog<KSaveSelectDialog> dialog(checkSave, qApp->activeWindow());
0858             return dialog->exec();
0859         }
0860     }
0861 
0862     return true;
0863 }
0864 
0865 QList< IDocument * > KDevelop::DocumentController::visibleDocumentsInWindow(MainWindow * mw) const
0866 {
0867     // Gather a list of all documents which do have a view in the given main window
0868     // Does not find documents which are open in inactive areas
0869     QList<IDocument*> list;
0870     const auto documents = openDocuments();
0871     for (IDocument* doc : documents) {
0872         if (auto* sdoc = dynamic_cast<Sublime::Document*>(doc)) {
0873             const auto views = sdoc->views();
0874             auto hasViewInWindow = std::any_of(views.begin(), views.end(), [&](Sublime::View* view) {
0875                 return (view->hasWidget() && view->widget()->window() == mw);
0876             });
0877             if (hasViewInWindow) {
0878                 list.append(doc);
0879             }
0880         }
0881     }
0882     return list;
0883 }
0884 
0885 QList< IDocument * > KDevelop::DocumentController::documentsExclusivelyInWindow(MainWindow * mw, bool currentAreaOnly) const
0886 {
0887     // Gather a list of all documents which have views only in the given main window
0888     QList<IDocument*> checkSave;
0889 
0890     const auto documents = openDocuments();
0891     for (IDocument* doc : documents) {
0892         if (auto* sdoc = dynamic_cast<Sublime::Document*>(doc)) {
0893             bool inOtherWindow = false;
0894 
0895             const auto views = sdoc->views();
0896             for (Sublime::View* view : views) {
0897                 const auto windows = Core::self()->uiControllerInternal()->mainWindows();
0898                 for (Sublime::MainWindow* window : windows) {
0899                     if(window->containsView(view) && (window != mw || (currentAreaOnly && window == mw && !mw->area()->views().contains(view)))) {
0900                         inOtherWindow = true;
0901                         break;
0902                     }
0903                 }
0904                 if (inOtherWindow) {
0905                     break;
0906                 }
0907             }
0908 
0909             if (!inOtherWindow)
0910                 checkSave.append(doc);
0911         }
0912     }
0913     return checkSave;
0914 }
0915 
0916 QList< IDocument * > KDevelop::DocumentController::modifiedDocuments(const QList< IDocument * > & list) const
0917 {
0918     QList< IDocument * > ret;
0919     for (IDocument* doc : list) {
0920         if (doc->state() == IDocument::Modified || doc->state() == IDocument::DirtyAndModified)
0921             ret.append(doc);
0922     }
0923     return ret;
0924 }
0925 
0926 bool DocumentController::saveAllDocumentsForWindow(KParts::MainWindow* mw, KDevelop::IDocument::DocumentSaveMode mode, bool currentAreaOnly)
0927 {
0928     QList<IDocument*> checkSave = documentsExclusivelyInWindow(qobject_cast<KDevelop::MainWindow*>(mw), currentAreaOnly);
0929 
0930     return saveSomeDocuments(checkSave, mode);
0931 }
0932 
0933 void DocumentController::reloadAllDocuments()
0934 {
0935     if (Sublime::MainWindow* mw = Core::self()->uiControllerInternal()->activeSublimeWindow()) {
0936         const QList<IDocument*> views = visibleDocumentsInWindow(qobject_cast<KDevelop::MainWindow*>(mw));
0937 
0938         if (!saveSomeDocuments(views, IDocument::Default))
0939             // User cancelled or other error
0940             return;
0941 
0942         for (IDocument* doc : views) {
0943             if(!isEmptyDocumentUrl(doc->url()))
0944                 doc->reload();
0945         }
0946     }
0947 }
0948 
0949 bool DocumentController::closeAllDocuments()
0950 {
0951     if (Sublime::MainWindow* mw = Core::self()->uiControllerInternal()->activeSublimeWindow()) {
0952         const QList<IDocument*> views = visibleDocumentsInWindow(qobject_cast<KDevelop::MainWindow*>(mw));
0953 
0954         if (!saveSomeDocuments(views, IDocument::Default))
0955             // User cancelled or other error
0956             return false;
0957 
0958         for (IDocument* doc : views) {
0959             doc->close(IDocument::Discard);
0960         }
0961     }
0962     return true;
0963 }
0964 
0965 void DocumentController::closeAllOtherDocuments()
0966 {
0967     if (Sublime::MainWindow* mw = Core::self()->uiControllerInternal()->activeSublimeWindow()) {
0968         Sublime::View* activeView = mw->activeView();
0969 
0970         if (!activeView) {
0971             qCWarning(SHELL) << "Shouldn't there always be an active view when this function is called?";
0972             return;
0973         }
0974 
0975         // Deal with saving unsaved solo views
0976         QList<IDocument*> soloViews = documentsExclusivelyInWindow(qobject_cast<KDevelop::MainWindow*>(mw));
0977         soloViews.removeAll(qobject_cast<IDocument*>(activeView->document()));
0978 
0979         if (!saveSomeDocuments(soloViews, IDocument::Default))
0980             // User cancelled or other error
0981             return;
0982 
0983         const auto views = mw->area()->views();
0984         for (Sublime::View* view : views) {
0985             if (view != activeView)
0986                 mw->area()->closeView(view);
0987         }
0988         activeView->widget()->setFocus();
0989     }
0990 }
0991 
0992 IDocument* DocumentController::activeDocument() const
0993 {
0994     UiController *uiController = Core::self()->uiControllerInternal();
0995     Sublime::MainWindow* mw = uiController->activeSublimeWindow();
0996     if( !mw || !mw->activeView() ) return nullptr;
0997     return qobject_cast<IDocument*>(mw->activeView()->document());
0998 }
0999 
1000 KTextEditor::View* DocumentController::activeTextDocumentView() const
1001 {
1002     UiController *uiController = Core::self()->uiControllerInternal();
1003     Sublime::MainWindow* mw = uiController->activeSublimeWindow();
1004     if( !mw || !mw->activeView() )
1005         return nullptr;
1006 
1007     auto* view = qobject_cast<TextView*>(mw->activeView());
1008     if(!view)
1009         return nullptr;
1010     return view->textView();
1011 }
1012 
1013 QString DocumentController::activeDocumentPath( const QString& target ) const
1014 {
1015     if(!target.isEmpty()) {
1016         const auto projects = Core::self()->projectController()->projects();
1017         for (IProject* project : projects) {
1018             if(project->name().startsWith(target, Qt::CaseInsensitive)) {
1019                 return project->path().pathOrUrl() + QLatin1String("/.");
1020             }
1021         }
1022     }
1023     IDocument* doc = activeDocument();
1024     if(!doc || target == QLatin1String("[selection]"))
1025     {
1026         Context* selection = ICore::self()->selectionController()->currentSelection();
1027         if(selection && selection->type() == Context::ProjectItemContext && !static_cast<ProjectItemContext*>(selection)->items().isEmpty())
1028         {
1029             QString ret = static_cast<ProjectItemContext*>(selection)->items().at(0)->path().pathOrUrl();
1030             if(static_cast<ProjectItemContext*>(selection)->items().at(0)->folder())
1031                 ret += QLatin1String("/.");
1032             return  ret;
1033         }
1034         return QString();
1035     }
1036     return doc->url().toString();
1037 }
1038 
1039 QStringList DocumentController::activeDocumentPaths() const
1040 {
1041     UiController *uiController = Core::self()->uiControllerInternal();
1042     if( !uiController->activeSublimeWindow() ) return QStringList();
1043 
1044     QSet<QString> documents;
1045     const auto views = uiController->activeSublimeWindow()->area()->views();
1046     for (Sublime::View* view : views) {
1047         documents.insert(view->document()->documentSpecifier());
1048     }
1049 
1050     return documents.values();
1051 }
1052 
1053 void DocumentController::registerDocumentForMimetype( const QString& mimetype,
1054                                         KDevelop::IDocumentFactory* factory )
1055 {
1056     Q_D(DocumentController);
1057 
1058     if( !d->factories.contains( mimetype ) )
1059         d->factories[mimetype] = factory;
1060 }
1061 
1062 QStringList DocumentController::documentTypes() const
1063 {
1064     return QStringList() << QStringLiteral("Text");
1065 }
1066 
1067 static const QRegularExpression& emptyDocumentPattern()
1068 {
1069     static const QRegularExpression pattern(QStringLiteral("^/%1(?:\\s\\((\\d+)\\))?$").arg(EMPTY_DOCUMENT_URL));
1070     return pattern;
1071 }
1072 
1073 bool DocumentController::isEmptyDocumentUrl(const QUrl &url)
1074 {
1075     return emptyDocumentPattern().match(url.toDisplayString(QUrl::PreferLocalFile)).hasMatch();
1076 }
1077 
1078 QUrl DocumentController::nextEmptyDocumentUrl()
1079 {
1080     int nextEmptyDocNumber = 0;
1081     const auto& pattern = emptyDocumentPattern();
1082     const auto openDocuments = Core::self()->documentControllerInternal()->openDocuments();
1083     for (IDocument* doc : openDocuments) {
1084         if (DocumentController::isEmptyDocumentUrl(doc->url())) {
1085             const auto match = pattern.match(doc->url().toDisplayString(QUrl::PreferLocalFile));
1086             if (match.hasMatch()) {
1087                 const int num = match.capturedRef(1).toInt();
1088                 nextEmptyDocNumber = qMax(nextEmptyDocNumber, num + 1);
1089             } else {
1090                 nextEmptyDocNumber = qMax(nextEmptyDocNumber, 1);
1091             }
1092         }
1093     }
1094 
1095     QUrl url;
1096     if (nextEmptyDocNumber > 0)
1097         url = QUrl::fromLocalFile(QStringLiteral("/%1 (%2)").arg(EMPTY_DOCUMENT_URL).arg(nextEmptyDocNumber));
1098     else
1099         url = QUrl::fromLocalFile(QLatin1Char('/') + EMPTY_DOCUMENT_URL);
1100     return url;
1101 }
1102 
1103 IDocumentFactory* DocumentController::factory(const QString& mime) const
1104 {
1105     Q_D(const DocumentController);
1106 
1107     return d->factories.value(mime);
1108 }
1109 
1110 bool DocumentController::openDocumentsSimple( QStringList urls )
1111 {
1112     Sublime::Area* area = Core::self()->uiControllerInternal()->activeArea();
1113     Sublime::AreaIndex* areaIndex = area->rootIndex();
1114 
1115     QList<Sublime::View*> topViews = static_cast<Sublime::MainWindow*>(Core::self()->uiControllerInternal()->activeMainWindow())->topViews();
1116 
1117     if(Sublime::View* activeView = Core::self()->uiControllerInternal()->activeSublimeWindow()->activeView())
1118         areaIndex = area->indexOf(activeView);
1119 
1120     qCDebug(SHELL) << "opening " << urls << " to area " << area << " index " << areaIndex << " with children " << areaIndex->first() << " " << areaIndex->second();
1121 
1122     bool isFirstView = true;
1123 
1124     bool ret = openDocumentsWithSplitSeparators( areaIndex, urls, isFirstView );
1125 
1126     qCDebug(SHELL) << "area arch. after opening: " << areaIndex->print();
1127 
1128     // Required because sublime sometimes doesn't update correctly when the area-index contents has been changed
1129     // (especially when views have been moved to other indices, through unsplit, split, etc.)
1130     static_cast<Sublime::MainWindow*>(Core::self()->uiControllerInternal()->activeMainWindow())->reconstructViews(topViews);
1131 
1132     return ret;
1133 }
1134 
1135 bool DocumentController::openDocumentsWithSplitSeparators( Sublime::AreaIndex* index, QStringList urlsWithSeparators, bool& isFirstView )
1136 {
1137     qCDebug(SHELL) << "opening " << urlsWithSeparators << " index " << index << " with children " << index->first() << " " << index->second() << " view-count " << index->viewCount();
1138     if(urlsWithSeparators.isEmpty())
1139         return true;
1140 
1141     Sublime::Area* area = Core::self()->uiControllerInternal()->activeArea();
1142 
1143     QList<int> topLevelSeparators; // Indices of the top-level separators (with groups skipped)
1144     const QStringList separators {QStringLiteral("/"), QStringLiteral("-")};
1145     QList<QStringList> groups;
1146 
1147     bool ret = true;
1148 
1149     {
1150         int parenDepth = 0;
1151         int groupStart = 0;
1152         for(int pos = 0; pos < urlsWithSeparators.size(); ++pos)
1153         {
1154             QString item = urlsWithSeparators[pos];
1155             if(separators.contains(item))
1156             {
1157                 if(parenDepth == 0)
1158                     topLevelSeparators << pos;
1159             }else if(item == QLatin1String("["))
1160             {
1161                 if(parenDepth == 0)
1162                     groupStart = pos+1;
1163                 ++parenDepth;
1164             }
1165             else if(item == QLatin1String("]"))
1166             {
1167                 if(parenDepth > 0)
1168                 {
1169                     --parenDepth;
1170 
1171                     if(parenDepth == 0)
1172                         groups << urlsWithSeparators.mid(groupStart, pos-groupStart);
1173                 }
1174                 else{
1175                     qCDebug(SHELL) << "syntax error in " << urlsWithSeparators << ": parens do not match";
1176                     ret = false;
1177                 }
1178             }else if(parenDepth == 0)
1179             {
1180                 groups << (QStringList() << item);
1181             }
1182         }
1183     }
1184 
1185     if(topLevelSeparators.isEmpty())
1186     {
1187         if(urlsWithSeparators.size() > 1)
1188         {
1189             for (const QStringList& group : qAsConst(groups)) {
1190                 ret &= openDocumentsWithSplitSeparators( index, group, isFirstView );
1191             }
1192         }else{
1193             const auto url = QUrl::fromUserInput(urlsWithSeparators.front());
1194 
1195             // The file name of a remote URL that ends with a slash is empty, but such a URL can still reference a file.
1196             // The opposite condition is asserted in DocumentControllerPrivate::openDocumentInternal(),
1197             // which is (indirectly) called below.
1198             if (url.isLocalFile() && url.fileName().isEmpty()) {
1199                 qCDebug(SHELL) << "cannot open a directory" << url.toString();
1200                 return false;
1201             }
1202 
1203             while(index->isSplit())
1204                 index = index->first();
1205             // Simply open the document into the area index
1206             IDocument* doc = Core::self()->documentControllerInternal()->openDocument(
1207                 url, KTextEditor::Cursor::invalid(),
1208                 IDocumentController::DoNotActivate | IDocumentController::DoNotCreateView);
1209             auto *sublimeDoc = dynamic_cast<Sublime::Document*>(doc);
1210             if (sublimeDoc) {
1211                 Sublime::View* view = sublimeDoc->createView();
1212                 area->addView(view, index);
1213                 if(isFirstView)
1214                 {
1215                     static_cast<Sublime::MainWindow*>(Core::self()->uiControllerInternal()->activeMainWindow())->activateView(view);
1216                     isFirstView = false;
1217                 }
1218             }else{
1219                 ret = false;
1220             }
1221         }
1222         return ret;
1223     }
1224 
1225     // Pick a separator in the middle
1226 
1227     int pickSeparator = topLevelSeparators[topLevelSeparators.size()/2];
1228 
1229     bool activeViewToSecondChild = false;
1230     if(pickSeparator == urlsWithSeparators.size()-1)
1231     {
1232         // There is no right child group, so the right side should be filled with the currently active views
1233         activeViewToSecondChild = true;
1234     }else{
1235         QStringList separatorsAndParens = separators;
1236         separatorsAndParens << QStringLiteral("[") << QStringLiteral("]");
1237         // Check if the second child-set contains an unterminated separator, which means that the active views should end up there
1238         for(int pos = pickSeparator+1; pos < urlsWithSeparators.size(); ++pos)
1239             if( separators.contains(urlsWithSeparators[pos]) && (pos == urlsWithSeparators.size()-1 ||
1240                 separatorsAndParens.contains(urlsWithSeparators[pos-1])) )
1241                     activeViewToSecondChild = true;
1242     }
1243 
1244     Qt::Orientation orientation = urlsWithSeparators[pickSeparator] == QLatin1String("/") ? Qt::Horizontal : Qt::Vertical;
1245 
1246     if(!index->isSplit())
1247     {
1248         qCDebug(SHELL) << "splitting " << index << "orientation" << orientation << "to second" << activeViewToSecondChild;
1249         index->split(orientation, activeViewToSecondChild);
1250     }else{
1251         index->setOrientation(orientation);
1252         qCDebug(SHELL) << "WARNING: Area is already split (shouldn't be)" << urlsWithSeparators;
1253     }
1254 
1255     openDocumentsWithSplitSeparators( index->first(), urlsWithSeparators.mid(0, pickSeparator) , isFirstView );
1256     if(pickSeparator != urlsWithSeparators.size() - 1)
1257         openDocumentsWithSplitSeparators( index->second(), urlsWithSeparators.mid(pickSeparator+1, urlsWithSeparators.size() - (pickSeparator+1) ), isFirstView );
1258 
1259     // Clean up the child-indices, because document-loading may fail
1260 
1261     if(!index->first()->viewCount() && !index->first()->isSplit())
1262     {
1263         qCDebug(SHELL) << "unsplitting first";
1264         index->unsplit(index->first());
1265     }
1266     else if(!index->second()->viewCount() && !index->second()->isSplit())
1267     {
1268         qCDebug(SHELL) << "unsplitting second";
1269         index->unsplit(index->second());
1270     }
1271 
1272     return ret;
1273 }
1274 
1275 void DocumentController::vcsAnnotateCurrentDocument()
1276 {
1277     IDocument* doc = activeDocument();
1278     if (!doc)
1279         return;
1280 
1281     QUrl url = doc->url();
1282     IProject* project = KDevelop::ICore::self()->projectController()->findProjectForUrl(url);
1283     if(project && project->versionControlPlugin()) {
1284         auto* iface = project->versionControlPlugin()->extension<IBasicVersionControl>();
1285         auto helper = new VcsPluginHelper(project->versionControlPlugin(), iface);
1286         connect(doc->textDocument(), &KTextEditor::Document::aboutToClose,
1287                 helper, QOverload<KTextEditor::Document*>::of(&VcsPluginHelper::disposeEventually));
1288         Q_ASSERT(qobject_cast<KTextEditor::AnnotationViewInterface*>(doc->activeTextView()));
1289         // can't use new signal slot syntax here, AnnotationViewInterface is not a QObject
1290         connect(doc->activeTextView(), SIGNAL(annotationBorderVisibilityChanged(View*,bool)),
1291                 helper, SLOT(disposeEventually(View*,bool)));
1292         helper->addContextDocument(url);
1293         helper->annotation();
1294     }
1295     else {
1296         const QString messageText =
1297             i18n("Could not annotate the document because it is not part of a version-controlled project.");
1298         auto* message = new Sublime::Message(messageText, Sublime::Message::Error);
1299         ICore::self()->uiController()->postMessage(message);
1300     }
1301 }
1302 
1303 #include "moc_documentcontroller.cpp"