From 730fd82ac84796419fe1f9fa1f075ba045b02595 Mon Sep 17 00:00:00 2001 From: Friedemann Kleint <Friedemann.Kleint@nokia.com> Date: Tue, 15 Dec 2009 14:20:06 +0100 Subject: [PATCH] VCS: Add "Open file" context menu action to VCS log pane to be used for status/opened output. enabling convenient opening. Append repository as block data to log text to be able to resolve relative paths. --- src/plugins/git/gitclient.cpp | 6 +- src/plugins/mercurial/mercurialclient.cpp | 4 + src/plugins/perforce/perforceeditor.cpp | 2 +- src/plugins/perforce/perforceplugin.cpp | 32 +++++- src/plugins/perforce/perforceplugin.h | 4 +- src/plugins/subversion/subversionplugin.cpp | 3 + src/plugins/vcsbase/vcsbaseoutputwindow.cpp | 117 +++++++++++++++++++- src/plugins/vcsbase/vcsbaseoutputwindow.h | 12 +- 8 files changed, 167 insertions(+), 13 deletions(-) diff --git a/src/plugins/git/gitclient.cpp b/src/plugins/git/gitclient.cpp index cca432cbabc..c7e4468b7b3 100644 --- a/src/plugins/git/gitclient.cpp +++ b/src/plugins/git/gitclient.cpp @@ -251,7 +251,11 @@ void GitClient::status(const QString &workingDirectory) // @TODO: Use "--no-color" once it is supported QStringList statusArgs(QLatin1String("status")); statusArgs << QLatin1String("-u"); - executeGit(workingDirectory, statusArgs, 0, true); + VCSBase::VCSBaseOutputWindow *outwin = VCSBase::VCSBaseOutputWindow::instance(); + outwin->setRepository(workingDirectory); + GitCommand *command = executeGit(workingDirectory, statusArgs, 0, true); + connect(command, SIGNAL(finished(bool,QVariant)), outwin, SLOT(clearRepository()), + Qt::QueuedConnection); } void GitClient::log(const QString &workingDirectory, const QStringList &fileNames) diff --git a/src/plugins/mercurial/mercurialclient.cpp b/src/plugins/mercurial/mercurialclient.cpp index fa5321630e5..45001d7191a 100644 --- a/src/plugins/mercurial/mercurialclient.cpp +++ b/src/plugins/mercurial/mercurialclient.cpp @@ -250,7 +250,11 @@ void MercurialClient::status(const QString &workingDir, const QString &file) QStringList args(QLatin1String("status")); if (!file.isEmpty()) args.append(file); + VCSBase::VCSBaseOutputWindow *outwin = VCSBase::VCSBaseOutputWindow::instance(); + outwin->setRepository(workingDir); QSharedPointer<HgTask> job(new HgTask(workingDir, args, false)); + connect(job.data(), SIGNAL(succeeded(QVariant)), outwin, SLOT(clearRepository()), + Qt::QueuedConnection); enqueueJob(job); } diff --git a/src/plugins/perforce/perforceeditor.cpp b/src/plugins/perforce/perforceeditor.cpp index 2c3621e5236..67f90446f91 100644 --- a/src/plugins/perforce/perforceeditor.cpp +++ b/src/plugins/perforce/perforceeditor.cpp @@ -145,7 +145,7 @@ QString PerforceEditor::fileNameFromDiffSpecification(const QTextBlock &inBlock) if (revisionPos != -1 && revisionPos < diffFileName.length() - 1) diffFileName.truncate(revisionPos); // Ask plugin to map back - const QString fileName = m_plugin->fileNameFromPerforceName(diffFileName.trimmed(), &errorMessage); + const QString fileName = m_plugin->fileNameFromPerforceName(diffFileName.trimmed(), false, &errorMessage); if (fileName.isEmpty()) qWarning("%s", qPrintable(errorMessage)); return fileName; diff --git a/src/plugins/perforce/perforceplugin.cpp b/src/plugins/perforce/perforceplugin.cpp index b95e69596ad..0ab8b079f51 100644 --- a/src/plugins/perforce/perforceplugin.cpp +++ b/src/plugins/perforce/perforceplugin.cpp @@ -544,8 +544,29 @@ void PerforcePlugin::updateCheckout(const QString &workingDir, const QStringList void PerforcePlugin::printOpenedFileList() { - runP4Cmd(m_settings.topLevel(), QStringList(QLatin1String("opened")), - CommandToWindow|StdOutToWindow|StdErrToWindow|ErrorToWindow); + const PerforceResponse perforceResponse + = runP4Cmd(m_settings.topLevel(), QStringList(QLatin1String("opened")), + CommandToWindow|StdErrToWindow|ErrorToWindow); + if (perforceResponse.error || perforceResponse.stdOut.isEmpty()) + return; + // reformat "//depot/file.cpp#1 - description" into "file.cpp # - description" + // for context menu opening to work. This produces absolute paths, then. + VCSBase::VCSBaseOutputWindow *outWin = VCSBase::VCSBaseOutputWindow::instance(); + QString errorMessage; + QString mapped; + const QChar delimiter = QLatin1Char('#'); + foreach (const QString &line, perforceResponse.stdOut.split(QLatin1Char('\n'))) { + mapped.clear(); + const int delimiterPos = line.indexOf(delimiter); + if (delimiterPos > 0) + mapped = fileNameFromPerforceName(line.left(delimiterPos), true, &errorMessage); + if (mapped.isEmpty()) { + outWin->appendSilently(line); + } else { + outWin->appendSilently(mapped + QLatin1Char(' ') + line.mid(delimiterPos)); + } + } + outWin->popup(); } void PerforcePlugin::startSubmitProject() @@ -1282,6 +1303,7 @@ static inline QString msgWhereFailed(const QString & file, const QString &why) // Map a perforce name "//xx" to its real name in the file system QString PerforcePlugin::fileNameFromPerforceName(const QString& perforceName, + bool quiet, QString *errorMessage) const { // All happy, already mapped @@ -1290,8 +1312,10 @@ QString PerforcePlugin::fileNameFromPerforceName(const QString& perforceName, // "where" remaps the file to client file tree QStringList args; args << QLatin1String("where") << perforceName; - const PerforceResponse response = runP4Cmd(m_settings.topLevelSymLinkTarget(), args, - RunFullySynchronous|CommandToWindow|StdErrToWindow|ErrorToWindow); + unsigned flags = RunFullySynchronous; + if (!quiet) + flags |= CommandToWindow|StdErrToWindow|ErrorToWindow; + const PerforceResponse response = runP4Cmd(m_settings.topLevelSymLinkTarget(), args, flags); if (response.error) { *errorMessage = msgWhereFailed(perforceName, response.message); return QString::null; diff --git a/src/plugins/perforce/perforceplugin.h b/src/plugins/perforce/perforceplugin.h index bd1813a93a4..f3a60d02e4a 100644 --- a/src/plugins/perforce/perforceplugin.h +++ b/src/plugins/perforce/perforceplugin.h @@ -96,7 +96,9 @@ public: void setSettings(const Settings &s); // Map a perforce name "//xx" to its real name in the file system - QString fileNameFromPerforceName(const QString& perforceName, QString *errorMessage) const; + QString fileNameFromPerforceName(const QString& perforceName, + bool quiet, + QString *errorMessage) const; public slots: void describe(const QString &source, const QString &n); diff --git a/src/plugins/subversion/subversionplugin.cpp b/src/plugins/subversion/subversionplugin.cpp index 545c5cf16ac..e648f3163b1 100644 --- a/src/plugins/subversion/subversionplugin.cpp +++ b/src/plugins/subversion/subversionplugin.cpp @@ -748,7 +748,10 @@ void SubversionPlugin::projectStatus() QTC_ASSERT(state.hasProject(), return); QStringList args(QLatin1String("status")); args += state.relativeCurrentProject(); + VCSBase::VCSBaseOutputWindow *outwin = VCSBase::VCSBaseOutputWindow::instance(); + outwin->setRepository(state.currentProjectTopLevel()); runSvn(state.currentProjectTopLevel(), args, m_settings.timeOutMS(), true); + outwin->clearRepository(); } void SubversionPlugin::describe(const QString &source, const QString &changeNr) diff --git a/src/plugins/vcsbase/vcsbaseoutputwindow.cpp b/src/plugins/vcsbase/vcsbaseoutputwindow.cpp index 90ee006285c..0f5812920f9 100644 --- a/src/plugins/vcsbase/vcsbaseoutputwindow.cpp +++ b/src/plugins/vcsbase/vcsbaseoutputwindow.cpp @@ -30,28 +30,44 @@ #include "vcsbaseoutputwindow.h" #include <utils/qtcassert.h> +#include <coreplugin/editormanager/editormanager.h> #include <QtGui/QPlainTextEdit> #include <QtGui/QTextCharFormat> #include <QtGui/QContextMenuEvent> +#include <QtGui/QTextBlock> #include <QtGui/QMenu> #include <QtGui/QAction> +#include <QtGui/QTextDocument> +#include <QtGui/QTextBlockUserData> #include <QtCore/QPointer> #include <QtCore/QTextCodec> #include <QtCore/QTime> +#include <QtCore/QPoint> +#include <QtCore/QFileInfo> namespace VCSBase { namespace Internal { +// Store repository along with text blocks +class RepositoryUserData : public QTextBlockUserData { +public: + explicit RepositoryUserData(const QString &repo) : m_repository(repo) {} + const QString &repository() const { return m_repository; } + +private: + const QString m_repository; +}; + // A plain text edit with a special context menu containing "Clear" and // and functions to append specially formatted entries. class OutputWindowPlainTextEdit : public QPlainTextEdit { public: explicit OutputWindowPlainTextEdit(QWidget *parent); - void appendLines(QString s); + void appendLines(QString s, const QString &repository = QString()); // Append red error text and pop up. void appendError(const QString &text); // Append warning error text and pop up. @@ -63,6 +79,8 @@ protected: virtual void contextMenuEvent(QContextMenuEvent *event); private: + QString identifierUnderCursor(const QPoint &pos, QString *repository = 0) const; + const QTextCharFormat m_defaultFormat; QTextCharFormat m_errorFormat; QTextCharFormat m_warningFormat; @@ -83,27 +101,100 @@ OutputWindowPlainTextEdit::OutputWindowPlainTextEdit(QWidget *parent) : m_commandFormat.setFontWeight(QFont::Bold); } +// Search back for beginning of word +static inline int firstWordCharacter(const QString &s, int startPos) +{ + for ( ; startPos >= 0 ; startPos--) { + if (s.at(startPos).isSpace()) + return startPos + 1; + } + return 0; +} + +QString OutputWindowPlainTextEdit::identifierUnderCursor(const QPoint &widgetPos, QString *repository) const +{ + if (repository) + repository->clear(); + // Get the blank-delimited word under cursor. Note that + // using "SelectWordUnderCursor" does not work since it breaks + // at delimiters like '/'. Get the whole line + QTextCursor cursor = cursorForPosition(widgetPos); + const int cursorDocumentPos = cursor.position(); + cursor.select(QTextCursor::BlockUnderCursor); + if (!cursor.hasSelection()) + return QString(); + QString block = cursor.selectedText(); + // Determine cursor position within line and find blank-delimited word + const int cursorPos = cursorDocumentPos - cursor.block().position(); + const int blockSize = block.size(); + if (cursorPos < 0 || cursorPos >= blockSize || block.at(cursorPos).isSpace()) + return QString(); + // Retrieve repository if desired + if (repository) + if (QTextBlockUserData *data = cursor.block().userData()) + *repository = static_cast<const RepositoryUserData*>(data)->repository(); + // Find first non-space character of word and find first non-space character past + const int startPos = firstWordCharacter(block, cursorPos); + int endPos = cursorPos; + for ( ; endPos < blockSize && !block.at(endPos).isSpace(); endPos++) ; + return endPos > startPos ? block.mid(startPos, endPos - startPos) : QString(); +} + void OutputWindowPlainTextEdit::contextMenuEvent(QContextMenuEvent *event) { QMenu *menu = createStandardContextMenu(); + // Add 'open file' + QString repository; + const QString token = identifierUnderCursor(event->pos(), &repository); + QAction *openAction = 0; + if (!token.isEmpty()) { + // Check for a file, expand via repository if relative + QFileInfo fi(token); + if (!repository.isEmpty() && !fi.isFile() && fi.isRelative()) + fi = QFileInfo(repository + QLatin1Char('/') + token); + if (fi.isFile()) { + menu->addSeparator(); + openAction = menu->addAction(VCSBaseOutputWindow::tr("Open \"%1\"").arg(fi.fileName())); + openAction->setData(fi.absoluteFilePath()); + } + } + // Add 'clear' menu->addSeparator(); QAction *clearAction = menu->addAction(VCSBaseOutputWindow::tr("Clear")); - connect(clearAction, SIGNAL(triggered()), this, SLOT(clear())); - menu->exec(event->globalPos()); + + // Run + QAction *action = menu->exec(event->globalPos()); + if (action) { + if (action == clearAction) { + clear(); + return; + } + if (action == openAction) { + const QString fileName = action->data().toString(); + Core::EditorManager::instance()->openEditor(fileName); + } + } delete menu; } -void OutputWindowPlainTextEdit::appendLines(QString s) +void OutputWindowPlainTextEdit::appendLines(QString s, const QString &repository) { if (s.isEmpty()) return; // Avoid additional new line character generated by appendPlainText if (s.endsWith(QLatin1Char('\n'))) s.truncate(s.size() - 1); + const int previousLineCount = document()->lineCount(); appendPlainText(s); // Scroll down moveCursor(QTextCursor::End); ensureCursorVisible(); + if (!repository.isEmpty()) { + // Associate repository with new data. + QTextBlock block = document()->findBlockByLineNumber(previousLineCount); + for ( ; block.isValid(); block = block.next()) + block.setUserData(new RepositoryUserData(repository)); + } } void OutputWindowPlainTextEdit::appendError(const QString &text) @@ -135,6 +226,7 @@ void OutputWindowPlainTextEdit::appendCommand(const QString &text) struct VCSBaseOutputWindowPrivate { static VCSBaseOutputWindow *instance; QPointer<Internal::OutputWindowPlainTextEdit> plainTextEdit; + QString repository; }; VCSBaseOutputWindow *VCSBaseOutputWindowPrivate::instance = 0; @@ -236,7 +328,7 @@ void VCSBaseOutputWindow::setData(const QByteArray &data) void VCSBaseOutputWindow::appendSilently(const QString &text) { QTC_ASSERT(d->plainTextEdit, return) - d->plainTextEdit->appendLines(text); + d->plainTextEdit->appendLines(text, d->repository); } void VCSBaseOutputWindow::append(const QString &text) @@ -292,4 +384,19 @@ VCSBaseOutputWindow *VCSBaseOutputWindow::instance() return VCSBaseOutputWindowPrivate::instance; } +QString VCSBaseOutputWindow::repository() const +{ + return d->repository; +} + +void VCSBaseOutputWindow::setRepository(const QString &r) +{ + d->repository = r; +} + +void VCSBaseOutputWindow::clearRepository() +{ + d->repository.clear(); +} + } // namespace VCSBase diff --git a/src/plugins/vcsbase/vcsbaseoutputwindow.h b/src/plugins/vcsbase/vcsbaseoutputwindow.h index b7baf202f7b..d669a3e3155 100644 --- a/src/plugins/vcsbase/vcsbaseoutputwindow.h +++ b/src/plugins/vcsbase/vcsbaseoutputwindow.h @@ -41,11 +41,16 @@ struct VCSBaseOutputWindowPrivate; /* Common OutputWindow for Version Control System command and other output. * Installed by the base plugin and accessible for the other plugins * via static instance()-accessor. Provides slots to append output with - * special formatting. */ + * special formatting. + * It is possible to associate a repository with plain log text, enabling + * an "Open" context menu action over relative file name tokens in the text + * (absolute paths will also work). This can be used for "status" logs, + * showing modified file names, allowing the user to open them. */ class VCSBASE_EXPORT VCSBaseOutputWindow : public Core::IOutputPane { Q_OBJECT + Q_PROPERTY(QString repository READ repository WRITE setRepository) public: virtual ~VCSBaseOutputWindow(); @@ -70,7 +75,12 @@ public: static VCSBaseOutputWindow *instance(); + QString repository() const; + public slots: + void setRepository(const QString &); + void clearRepository(); + // Set the whole text. void setText(const QString &text); // Set text from QProcess' output data using the Locale's converter. -- GitLab