You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
601 lines
18 KiB
601 lines
18 KiB
/* |
|
Copyright 2007-2008 by Robert Knight <robertknight@gmail.com> |
|
|
|
This program is free software; you can redistribute it and/or modify |
|
it under the terms of the GNU General Public License as published by |
|
the Free Software Foundation; either version 2 of the License, or |
|
(at your option) any later version. |
|
|
|
This program is distributed in the hope that it will be useful, |
|
but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
GNU General Public License for more details. |
|
|
|
You should have received a copy of the GNU General Public License |
|
along with this program; if not, write to the Free Software |
|
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA |
|
02110-1301 USA. |
|
*/ |
|
|
|
// Own |
|
#include "Filter.h" |
|
|
|
#include "konsoledebug.h" |
|
#include <algorithm> |
|
|
|
// Qt |
|
#include <QAction> |
|
#include <QApplication> |
|
#include <QClipboard> |
|
#include <QDir> |
|
#include <QMimeDatabase> |
|
#include <QString> |
|
#include <QTextStream> |
|
#include <QUrl> |
|
#include <QMenu> |
|
|
|
// KDE |
|
#include <KLocalizedString> |
|
#include <KRun> |
|
#include <KFileItem> |
|
#include <KFileItemListProperties> |
|
#include <KFileItemActions> |
|
|
|
// Konsole |
|
#include "Session.h" |
|
#include "TerminalCharacterDecoder.h" |
|
#include "ProfileManager.h" |
|
#include "SessionManager.h" |
|
|
|
using namespace Konsole; |
|
|
|
FilterChain::~FilterChain() |
|
{ |
|
qDeleteAll(_filters); |
|
} |
|
|
|
void FilterChain::addFilter(Filter *filter) |
|
{ |
|
_filters.append(filter); |
|
} |
|
|
|
void FilterChain::removeFilter(Filter *filter) |
|
{ |
|
_filters.removeAll(filter); |
|
} |
|
|
|
void FilterChain::reset() |
|
{ |
|
for(auto *filter : _filters) { |
|
filter->reset(); |
|
} |
|
} |
|
|
|
void FilterChain::setBuffer(const QString *buffer, const QList<int> *linePositions) |
|
{ |
|
for(auto *filter : _filters) { |
|
filter->setBuffer(buffer, linePositions); |
|
} |
|
} |
|
|
|
void FilterChain::process() |
|
{ |
|
for( auto *filter : _filters) { |
|
filter->process(); |
|
} |
|
} |
|
|
|
void FilterChain::clear() |
|
{ |
|
_filters.clear(); |
|
} |
|
|
|
QSharedPointer<Filter::HotSpot> FilterChain::hotSpotAt(int line, int column) const |
|
{ |
|
for(auto *filter : _filters) { |
|
QSharedPointer<Filter::HotSpot> spot = filter->hotSpotAt(line, column); |
|
if (spot != nullptr) { |
|
return spot; |
|
} |
|
} |
|
return nullptr; |
|
} |
|
|
|
QList<QSharedPointer<Filter::HotSpot>> FilterChain::hotSpots() const |
|
{ |
|
QList<QSharedPointer<Filter::HotSpot>> list; |
|
for (auto *filter : _filters) { |
|
list.append(filter->hotSpots()); |
|
} |
|
return list; |
|
} |
|
|
|
TerminalImageFilterChain::TerminalImageFilterChain() : |
|
_buffer(nullptr), |
|
_linePositions(nullptr) |
|
{ |
|
} |
|
|
|
TerminalImageFilterChain::~TerminalImageFilterChain() = default; |
|
|
|
void TerminalImageFilterChain::setImage(const Character * const image, int lines, int columns, |
|
const QVector<LineProperty> &lineProperties) |
|
{ |
|
if (_filters.empty()) { |
|
return; |
|
} |
|
|
|
// reset all filters and hotspots |
|
reset(); |
|
|
|
PlainTextDecoder decoder; |
|
decoder.setLeadingWhitespace(true); |
|
decoder.setTrailingWhitespace(true); |
|
|
|
// setup new shared buffers for the filters to process on |
|
_buffer.reset(new QString()); |
|
_linePositions.reset(new QList<int>()); |
|
|
|
setBuffer(_buffer.get(), _linePositions.get()); |
|
|
|
QTextStream lineStream(_buffer.get()); |
|
decoder.begin(&lineStream); |
|
|
|
for (int i = 0; i < lines; i++) { |
|
_linePositions->append(_buffer->length()); |
|
decoder.decodeLine(image + i * columns, columns, LINE_DEFAULT); |
|
|
|
// pretend that each line ends with a newline character. |
|
// this prevents a link that occurs at the end of one line |
|
// being treated as part of a link that occurs at the start of the next line |
|
// |
|
// the downside is that links which are spread over more than one line are not |
|
// highlighted. |
|
// |
|
// TODO - Use the "line wrapped" attribute associated with lines in a |
|
// terminal image to avoid adding this imaginary character for wrapped |
|
// lines |
|
if ((lineProperties.value(i, LINE_DEFAULT) & LINE_WRAPPED) == 0) { |
|
lineStream << QLatin1Char('\n'); |
|
} |
|
} |
|
decoder.end(); |
|
} |
|
|
|
Filter::Filter() : |
|
_linePositions(nullptr), |
|
_buffer(nullptr) |
|
{ |
|
} |
|
|
|
Filter::~Filter() |
|
{ |
|
reset(); |
|
} |
|
|
|
void Filter::reset() |
|
{ |
|
_hotspots.clear(); |
|
_hotspotList.clear(); |
|
} |
|
|
|
void Filter::setBuffer(const QString *buffer, const QList<int> *linePositions) |
|
{ |
|
_buffer = buffer; |
|
_linePositions = linePositions; |
|
} |
|
|
|
std::pair<int, int> Filter::getLineColumn(int position) |
|
{ |
|
Q_ASSERT(_linePositions); |
|
Q_ASSERT(_buffer); |
|
|
|
for (int i = 0; i < _linePositions->count(); i++) { |
|
const int nextLine = i == _linePositions->count() - 1 |
|
? _buffer->length() + 1 |
|
: _linePositions->value(i + 1); |
|
|
|
if (_linePositions->value(i) <= position && position < nextLine) { |
|
return std::make_pair(i, Character::stringWidth(buffer()->mid(_linePositions->value(i), |
|
position - _linePositions->value(i)))); |
|
} |
|
} |
|
return std::make_pair(-1, -1); |
|
} |
|
|
|
const QString *Filter::buffer() |
|
{ |
|
return _buffer; |
|
} |
|
|
|
Filter::HotSpot::~HotSpot() = default; |
|
|
|
void Filter::addHotSpot(QSharedPointer<HotSpot> spot) |
|
{ |
|
_hotspotList << spot; |
|
|
|
for (int line = spot->startLine(); line <= spot->endLine(); line++) { |
|
_hotspots.insert(line, spot); |
|
} |
|
} |
|
|
|
QList<QSharedPointer<Filter::HotSpot>> Filter::hotSpots() const |
|
{ |
|
return _hotspotList; |
|
} |
|
|
|
QSharedPointer<Filter::HotSpot> Filter::hotSpotAt(int line, int column) const |
|
{ |
|
const auto hotspots = _hotspots.values(line); |
|
|
|
for (auto &spot : hotspots) { |
|
if (spot->startLine() == line && spot->startColumn() > column) { |
|
continue; |
|
} |
|
if (spot->endLine() == line && spot->endColumn() < column) { |
|
continue; |
|
} |
|
|
|
return spot; |
|
} |
|
|
|
return nullptr; |
|
} |
|
|
|
Filter::HotSpot::HotSpot(int startLine, int startColumn, int endLine, int endColumn) : |
|
_startLine(startLine), |
|
_startColumn(startColumn), |
|
_endLine(endLine), |
|
_endColumn(endColumn), |
|
_type(NotSpecified) |
|
{ |
|
} |
|
|
|
QList<QAction *> Filter::HotSpot::actions() |
|
{ |
|
return {}; |
|
} |
|
|
|
void Filter::HotSpot::setupMenu(QMenu *) |
|
{ |
|
|
|
} |
|
|
|
int Filter::HotSpot::startLine() const |
|
{ |
|
return _startLine; |
|
} |
|
|
|
int Filter::HotSpot::endLine() const |
|
{ |
|
return _endLine; |
|
} |
|
|
|
int Filter::HotSpot::startColumn() const |
|
{ |
|
return _startColumn; |
|
} |
|
|
|
int Filter::HotSpot::endColumn() const |
|
{ |
|
return _endColumn; |
|
} |
|
|
|
Filter::HotSpot::Type Filter::HotSpot::type() const |
|
{ |
|
return _type; |
|
} |
|
|
|
void Filter::HotSpot::setType(Type type) |
|
{ |
|
_type = type; |
|
} |
|
|
|
RegExpFilter::RegExpFilter() : |
|
_searchText(QRegularExpression()) |
|
{ |
|
} |
|
|
|
RegExpFilter::HotSpot::HotSpot(int startLine, int startColumn, int endLine, int endColumn, |
|
const QStringList &capturedTexts) : |
|
Filter::HotSpot(startLine, startColumn, endLine, endColumn), |
|
_capturedTexts(capturedTexts) |
|
{ |
|
setType(Marker); |
|
} |
|
|
|
void RegExpFilter::HotSpot::activate(QObject *) |
|
{ |
|
} |
|
|
|
QStringList RegExpFilter::HotSpot::capturedTexts() const |
|
{ |
|
return _capturedTexts; |
|
} |
|
|
|
void RegExpFilter::setRegExp(const QRegularExpression ®Exp) |
|
{ |
|
_searchText = regExp; |
|
_searchText.optimize(); |
|
} |
|
|
|
QRegularExpression RegExpFilter::regExp() const |
|
{ |
|
return _searchText; |
|
} |
|
|
|
void RegExpFilter::process() |
|
{ |
|
const QString *text = buffer(); |
|
|
|
Q_ASSERT(text); |
|
|
|
if (!_searchText.isValid() || _searchText.pattern().isEmpty()) { |
|
return; |
|
} |
|
|
|
QRegularExpressionMatchIterator iterator(_searchText.globalMatch(*text)); |
|
while (iterator.hasNext()) { |
|
QRegularExpressionMatch match(iterator.next()); |
|
std::pair<int, int> start = getLineColumn(match.capturedStart()); |
|
std::pair<int, int> end = getLineColumn(match.capturedEnd()); |
|
|
|
QSharedPointer<Filter::HotSpot> spot( |
|
newHotSpot(start.first, start.second, |
|
end.first, end.second, |
|
match.capturedTexts() |
|
) |
|
); |
|
|
|
if (spot == nullptr) { |
|
continue; |
|
} |
|
|
|
addHotSpot(spot); |
|
} |
|
} |
|
|
|
QSharedPointer<Filter::HotSpot> RegExpFilter::newHotSpot(int startLine, int startColumn, int endLine, |
|
int endColumn, const QStringList &capturedTexts) |
|
{ |
|
return QSharedPointer<Filter::HotSpot>(new RegExpFilter::HotSpot(startLine, startColumn, |
|
endLine, endColumn, capturedTexts)); |
|
} |
|
|
|
QSharedPointer<Filter::HotSpot> UrlFilter::newHotSpot(int startLine, int startColumn, int endLine, |
|
int endColumn, const QStringList &capturedTexts) |
|
{ |
|
return QSharedPointer<Filter::HotSpot>(new UrlFilter::HotSpot(startLine, startColumn, |
|
endLine, endColumn, capturedTexts)); |
|
} |
|
|
|
UrlFilter::HotSpot::HotSpot(int startLine, int startColumn, int endLine, int endColumn, |
|
const QStringList &capturedTexts) : |
|
RegExpFilter::HotSpot(startLine, startColumn, endLine, endColumn, capturedTexts) |
|
{ |
|
setType(Link); |
|
} |
|
|
|
UrlFilter::HotSpot::UrlType UrlFilter::HotSpot::urlType() const |
|
{ |
|
const QString url = capturedTexts().at(0); |
|
return FullUrlRegExp.match(url).hasMatch() ? StandardUrl |
|
: EmailAddressRegExp.match(url).hasMatch() ? Email |
|
: Unknown; |
|
} |
|
|
|
void UrlFilter::HotSpot::activate(QObject *object) |
|
{ |
|
QString url = capturedTexts().at(0); |
|
|
|
const UrlType kind = urlType(); |
|
|
|
const QString &actionName = object != nullptr ? object->objectName() : QString(); |
|
|
|
if (actionName == QLatin1String("copy-action")) { |
|
QApplication::clipboard()->setText(url); |
|
return; |
|
} |
|
|
|
if ((object == nullptr) || actionName == QLatin1String("open-action")) { |
|
if (kind == StandardUrl) { |
|
// if the URL path does not include the protocol ( eg. "www.kde.org" ) then |
|
// prepend https:// ( eg. "www.kde.org" --> "https://www.kde.org" ) |
|
if (!url.contains(QLatin1String("://"))) { |
|
url.prepend(QLatin1String("https://")); |
|
} |
|
} else if (kind == Email) { |
|
url.prepend(QLatin1String("mailto:")); |
|
} |
|
|
|
new KRun(QUrl(url), QApplication::activeWindow()); |
|
} |
|
} |
|
|
|
// Note: Altering these regular expressions can have a major effect on the performance of the filters |
|
// used for finding URLs in the text, especially if they are very general and could match very long |
|
// pieces of text. |
|
// Please be careful when altering them. |
|
|
|
//regexp matches: |
|
// full url: |
|
// protocolname:// or www. followed by anything other than whitespaces, <, >, ' or ", and ends before whitespaces, <, >, ', ", ], !, ), :, comma and dot |
|
const QRegularExpression UrlFilter::FullUrlRegExp(QStringLiteral("(www\\.(?!\\.)|[a-z][a-z0-9+.-]*://)[^\\s<>'\"]+[^!,\\.\\s<>'\"\\]\\)\\:]"), |
|
QRegularExpression::OptimizeOnFirstUsageOption); |
|
// email address: |
|
// [word chars, dots or dashes]@[word chars, dots or dashes].[word chars] |
|
const QRegularExpression UrlFilter::EmailAddressRegExp(QStringLiteral("\\b(\\w|\\.|-|\\+)+@(\\w|\\.|-)+\\.\\w+\\b"), |
|
QRegularExpression::OptimizeOnFirstUsageOption); |
|
|
|
// matches full url or email address |
|
const QRegularExpression UrlFilter::CompleteUrlRegExp(QLatin1Char('(') + FullUrlRegExp.pattern() + QLatin1Char('|') |
|
+ EmailAddressRegExp.pattern() + QLatin1Char(')'), |
|
QRegularExpression::OptimizeOnFirstUsageOption); |
|
|
|
UrlFilter::UrlFilter() |
|
{ |
|
setRegExp(CompleteUrlRegExp); |
|
} |
|
|
|
UrlFilter::HotSpot::~HotSpot() = default; |
|
|
|
QList<QAction *> UrlFilter::HotSpot::actions() |
|
{ |
|
auto openAction = new QAction(this); |
|
auto copyAction = new QAction(this); |
|
|
|
const UrlType kind = urlType(); |
|
Q_ASSERT(kind == StandardUrl || kind == Email); |
|
|
|
if (kind == StandardUrl) { |
|
openAction->setText(i18n("Open Link")); |
|
openAction->setIcon(QIcon::fromTheme(QStringLiteral("internet-services"))); |
|
copyAction->setText(i18n("Copy Link Address")); |
|
copyAction->setIcon(QIcon::fromTheme(QStringLiteral("edit-copy-url"))); |
|
} else if (kind == Email) { |
|
openAction->setText(i18n("Send Email To...")); |
|
openAction->setIcon(QIcon::fromTheme(QStringLiteral("mail-send"))); |
|
copyAction->setText(i18n("Copy Email Address")); |
|
copyAction->setIcon(QIcon::fromTheme(QStringLiteral("edit-copy-mail"))); |
|
} |
|
|
|
// object names are set here so that the hotspot performs the |
|
// correct action when activated() is called with the triggered |
|
// action passed as a parameter. |
|
openAction->setObjectName(QStringLiteral("open-action")); |
|
copyAction->setObjectName(QStringLiteral("copy-action")); |
|
|
|
QObject::connect(openAction, &QAction::triggered, this, [this, openAction]{ activate(openAction); }); |
|
QObject::connect(copyAction, &QAction::triggered, this, [this, copyAction]{ activate(copyAction); }); |
|
|
|
return {openAction, copyAction}; |
|
} |
|
|
|
/** |
|
* File Filter - Construct a filter that works on local file paths using the |
|
* posix portable filename character set combined with KDE's mimetype filename |
|
* extension blob patterns. |
|
* https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap03.html#tag_03_267 |
|
*/ |
|
|
|
QSharedPointer<Filter::HotSpot> FileFilter::newHotSpot(int startLine, int startColumn, int endLine, |
|
int endColumn, const QStringList &capturedTexts) |
|
{ |
|
if (_session.isNull()) { |
|
qCDebug(KonsoleDebug) << "Trying to create new hot spot without session!"; |
|
return nullptr; |
|
} |
|
|
|
QString filename = capturedTexts.first(); |
|
if (filename.startsWith(QLatin1Char('\'')) && filename.endsWith(QLatin1Char('\''))) { |
|
filename.remove(0, 1); |
|
filename.chop(1); |
|
} |
|
|
|
// Return nullptr if it's not: |
|
// <current dir>/filename |
|
// <current dir>/childDir/filename |
|
if (!_currentDirContents.contains(filename)) { |
|
return nullptr; |
|
} |
|
|
|
return QSharedPointer<Filter::HotSpot>(new FileFilter::HotSpot(startLine, startColumn, endLine, endColumn, capturedTexts, _dirPath + filename)); |
|
} |
|
|
|
void FileFilter::process() |
|
{ |
|
const QDir dir(_session->currentWorkingDirectory()); |
|
// Do not re-process. |
|
if (_dirPath != dir.canonicalPath() + QLatin1Char('/')) { |
|
_dirPath = dir.canonicalPath() + QLatin1Char('/'); |
|
#if QT_VERSION >= QT_VERSION_CHECK(5,14,0) |
|
|
|
const auto tmpList = dir.entryList(QDir::Dirs | QDir::Files); |
|
_currentDirContents = QSet<QString>(std::begin(tmpList), std::end(tmpList)); |
|
|
|
#else |
|
_currentDirContents = QSet<QString>::fromList(dir.entryList(QDir::Dirs | QDir::Files)); |
|
#endif |
|
} |
|
|
|
RegExpFilter::process(); |
|
} |
|
|
|
FileFilter::HotSpot::HotSpot(int startLine, int startColumn, int endLine, int endColumn, |
|
const QStringList &capturedTexts, const QString &filePath) : |
|
RegExpFilter::HotSpot(startLine, startColumn, endLine, endColumn, capturedTexts), |
|
_filePath(filePath) |
|
{ |
|
setType(Link); |
|
} |
|
|
|
void FileFilter::HotSpot::activate(QObject *) |
|
{ |
|
new KRun(QUrl::fromLocalFile(_filePath), QApplication::activeWindow()); |
|
} |
|
|
|
FileFilter::FileFilter(Session *session) : |
|
_session(session) |
|
, _dirPath(QString()) |
|
, _currentDirContents() |
|
{ |
|
Profile::Ptr profile = SessionManager::instance()->sessionProfile(_session); |
|
QString wordCharacters = profile->wordCharacters(); |
|
|
|
/* The wordCharacters can be a potentially broken regexp, |
|
* so let's fix it manually if it has some troublesome characters. |
|
*/ |
|
// Add a folder delimiter at the beginning. |
|
if (wordCharacters.contains(QLatin1Char('/'))) { |
|
wordCharacters.remove(QLatin1Char('/')); |
|
wordCharacters.prepend(QStringLiteral("\\/")); |
|
} |
|
|
|
// Add minus at the end. |
|
if (wordCharacters.contains(QLatin1Char('-'))){ |
|
wordCharacters.remove(QLatin1Char('-')); |
|
wordCharacters.append(QLatin1Char('-')); |
|
} |
|
|
|
static auto re = QRegularExpression( |
|
/* First part of the regexp means 'strings with spaces and starting with single quotes' |
|
* Second part means "Strings with double quotes" |
|
* Last part means "Everything else plus some special chars |
|
* This is much smaller, and faster, than the previous regexp |
|
* on the HotSpot creation we verify if this is indeed a file, so there's |
|
* no problem on testing on random words on the screen. |
|
*/ |
|
QLatin1String("'[^']+'") // Matches everything between single quotes. |
|
+ QStringLiteral(R"RX(|"[^"]+")RX") // Matches everything inside double quotes |
|
+ QStringLiteral(R"RX(|[\w%1]+)RX").arg(wordCharacters) // matches a contiguous line of alphanumeric characters plus some special ones defined in the profile. |
|
, |
|
QRegularExpression::DontCaptureOption); |
|
setRegExp(re); |
|
} |
|
|
|
FileFilter::HotSpot::~HotSpot() = default; |
|
|
|
QList<QAction *> FileFilter::HotSpot::actions() |
|
{ |
|
return {}; |
|
} |
|
|
|
void FileFilter::HotSpot::setupMenu(QMenu *menu) |
|
{ |
|
// We are reusing the QMenu, but we need to update the actions anyhow. |
|
// Remove the 'Open with' actions from it, then add the new ones. |
|
QList<QAction*> toDelete; |
|
for (auto *action : menu->actions()) { |
|
if (action->text().toLower().remove(QLatin1Char('&')).contains(i18n("open with"))) { |
|
toDelete.append(action); |
|
} |
|
} |
|
qDeleteAll(toDelete); |
|
|
|
const KFileItem fileItem(QUrl::fromLocalFile(_filePath)); |
|
const KFileItemList itemList({fileItem}); |
|
const KFileItemListProperties itemProperties(itemList); |
|
_menuActions.setParent(this); |
|
_menuActions.setItemListProperties(itemProperties); |
|
_menuActions.addOpenWithActionsTo(menu); |
|
}
|
|
|