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"