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.
711 lines
21 KiB
711 lines
21 KiB
/*
|
|
Copyright (C) 2001, S.R.Haque <srhaque@iee.org>.
|
|
Copyright (C) 2002, David Faure <david@mandrakesoft.com>
|
|
Copyright (C) 2004, Arend van Beelen jr. <arend@auton.nl>
|
|
This file is part of the KDE project
|
|
|
|
This library is free software; you can redistribute it and/or
|
|
modify it under the terms of the GNU Library General Public
|
|
License version 2, as published by the Free Software Foundation.
|
|
|
|
This library is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
Library General Public License for more details.
|
|
|
|
You should have received a copy of the GNU Library General Public License
|
|
along with this library; see the file COPYING.LIB. If not, write to
|
|
the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
|
|
Boston, MA 02110-1301, USA.
|
|
*/
|
|
|
|
#include "kfind.h"
|
|
#include "kfinddialog.h"
|
|
#include <kapplication.h>
|
|
#include <klocale.h>
|
|
#include <kmessagebox.h>
|
|
#include <qlabel.h>
|
|
#include <qregexp.h>
|
|
#include <qstylesheet.h>
|
|
#include <qguardedptr.h>
|
|
#include <qptrvector.h>
|
|
#include <kdebug.h>
|
|
|
|
//#define DEBUG_FIND
|
|
|
|
#define INDEX_NOMATCH -1
|
|
|
|
class KFindNextDialog : public KDialogBase
|
|
{
|
|
public:
|
|
KFindNextDialog(const QString &pattern, QWidget *parent);
|
|
};
|
|
|
|
// Create the dialog.
|
|
KFindNextDialog::KFindNextDialog(const QString &pattern, QWidget *parent) :
|
|
KDialogBase(parent, 0, false, // non-modal!
|
|
i18n("Find Next"),
|
|
User1 | Close,
|
|
User1,
|
|
false,
|
|
KStdGuiItem::find())
|
|
{
|
|
setMainWidget( new QLabel( i18n("<qt>Find next occurrence of '<b>%1</b>'?</qt>").arg(pattern), this ) );
|
|
}
|
|
|
|
////
|
|
|
|
struct KFind::Private
|
|
{
|
|
Private() :
|
|
findDialog(0),
|
|
patternChanged(false),
|
|
matchedPattern(""),
|
|
incrementalPath(29, true),
|
|
emptyMatch(0),
|
|
currentId(0),
|
|
customIds(false)
|
|
{
|
|
incrementalPath.setAutoDelete(true);
|
|
data.setAutoDelete(true);
|
|
}
|
|
|
|
~Private()
|
|
{
|
|
delete emptyMatch;
|
|
emptyMatch = 0;
|
|
}
|
|
|
|
struct Match
|
|
{
|
|
Match(int dataId, int index, int matchedLength) :
|
|
dataId(dataId),
|
|
index(index),
|
|
matchedLength(matchedLength)
|
|
{ }
|
|
|
|
int dataId;
|
|
int index;
|
|
int matchedLength;
|
|
};
|
|
|
|
struct Data
|
|
{
|
|
Data() : id(-1), dirty(false) { }
|
|
Data(int id, const QString &text, bool dirty = false) :
|
|
id(id),
|
|
text(text),
|
|
dirty(dirty)
|
|
{ }
|
|
|
|
int id;
|
|
QString text;
|
|
bool dirty;
|
|
};
|
|
|
|
QGuardedPtr<QWidget> findDialog;
|
|
bool patternChanged;
|
|
QString matchedPattern;
|
|
QDict<Match> incrementalPath;
|
|
Match * emptyMatch;
|
|
QPtrVector<Data> data;
|
|
int currentId;
|
|
bool customIds;
|
|
};
|
|
|
|
////
|
|
|
|
KFind::KFind( const QString &pattern, long options, QWidget *parent )
|
|
: QObject( parent )
|
|
{
|
|
d = new KFind::Private;
|
|
m_options = options;
|
|
init( pattern );
|
|
}
|
|
|
|
KFind::KFind( const QString &pattern, long options, QWidget *parent, QWidget *findDialog )
|
|
: QObject( parent )
|
|
{
|
|
d = new KFind::Private;
|
|
d->findDialog = findDialog;
|
|
m_options = options;
|
|
init( pattern );
|
|
}
|
|
|
|
void KFind::init( const QString& pattern )
|
|
{
|
|
m_matches = 0;
|
|
m_pattern = pattern;
|
|
m_dialog = 0;
|
|
m_dialogClosed = false;
|
|
m_index = INDEX_NOMATCH;
|
|
m_lastResult = NoMatch;
|
|
if (m_options & KFindDialog::RegularExpression)
|
|
m_regExp = new QRegExp(pattern, m_options & KFindDialog::CaseSensitive);
|
|
else {
|
|
m_regExp = 0;
|
|
}
|
|
}
|
|
|
|
KFind::~KFind()
|
|
{
|
|
delete m_dialog;
|
|
delete d;
|
|
}
|
|
|
|
bool KFind::needData() const
|
|
{
|
|
// always true when m_text is empty.
|
|
if (m_options & KFindDialog::FindBackwards)
|
|
// m_index==-1 and m_lastResult==Match means we haven't answered nomatch yet
|
|
// This is important in the "replace with a prompt" case.
|
|
return ( m_index < 0 && m_lastResult != Match );
|
|
else
|
|
// "index over length" test removed: we want to get a nomatch before we set data again
|
|
// This is important in the "replace with a prompt" case.
|
|
return m_index == INDEX_NOMATCH;
|
|
}
|
|
|
|
void KFind::setData( const QString& data, int startPos )
|
|
{
|
|
setData( -1, data, startPos );
|
|
}
|
|
|
|
void KFind::setData( int id, const QString& data, int startPos )
|
|
{
|
|
// cache the data for incremental find
|
|
if ( m_options & KFindDialog::FindIncremental )
|
|
{
|
|
if ( id != -1 )
|
|
d->customIds = true;
|
|
else
|
|
id = d->currentId + 1;
|
|
|
|
if ( id >= (int) d->data.size() )
|
|
d->data.resize( id + 100 );
|
|
|
|
d->data.insert( id, new Private::Data(id, data, true) );
|
|
}
|
|
|
|
if ( !(m_options & KFindDialog::FindIncremental) || needData() )
|
|
{
|
|
m_text = data;
|
|
|
|
if ( startPos != -1 )
|
|
m_index = startPos;
|
|
else if (m_options & KFindDialog::FindBackwards)
|
|
m_index = m_text.length();
|
|
else
|
|
m_index = 0;
|
|
#ifdef DEBUG_FIND
|
|
kdDebug() << "setData: '" << m_text << "' m_index=" << m_index << endl;
|
|
#endif
|
|
Q_ASSERT( m_index != INDEX_NOMATCH );
|
|
m_lastResult = NoMatch;
|
|
|
|
d->currentId = id;
|
|
}
|
|
}
|
|
|
|
KDialogBase* KFind::findNextDialog( bool create )
|
|
{
|
|
if ( !m_dialog && create )
|
|
{
|
|
m_dialog = new KFindNextDialog( m_pattern, parentWidget() );
|
|
connect( m_dialog, SIGNAL( user1Clicked() ), this, SLOT( slotFindNext() ) );
|
|
connect( m_dialog, SIGNAL( finished() ), this, SLOT( slotDialogClosed() ) );
|
|
}
|
|
return m_dialog;
|
|
}
|
|
|
|
KFind::Result KFind::find()
|
|
{
|
|
Q_ASSERT( m_index != INDEX_NOMATCH || d->patternChanged );
|
|
|
|
if ( m_lastResult == Match && !d->patternChanged )
|
|
{
|
|
// Move on before looking for the next match, _if_ we just found a match
|
|
if (m_options & KFindDialog::FindBackwards) {
|
|
m_index--;
|
|
if ( m_index == -1 ) // don't call KFind::find with -1, it has a special meaning
|
|
{
|
|
m_lastResult = NoMatch;
|
|
return NoMatch;
|
|
}
|
|
} else
|
|
m_index++;
|
|
}
|
|
d->patternChanged = false;
|
|
|
|
if ( m_options & KFindDialog::FindIncremental )
|
|
{
|
|
// if the current pattern is shorter than the matchedPattern we can
|
|
// probably look up the match in the incrementalPath
|
|
if ( m_pattern.length() < d->matchedPattern.length() )
|
|
{
|
|
Private::Match *match = m_pattern.isEmpty() ? d->emptyMatch : d->incrementalPath[m_pattern];
|
|
QString previousPattern = d->matchedPattern;
|
|
d->matchedPattern = m_pattern;
|
|
if ( match != 0 )
|
|
{
|
|
bool clean = true;
|
|
|
|
// find the first result backwards on the path that isn't dirty
|
|
while ( d->data[match->dataId]->dirty == true &&
|
|
!m_pattern.isEmpty() )
|
|
{
|
|
m_pattern.truncate( m_pattern.length() - 1 );
|
|
|
|
match = d->incrementalPath[m_pattern];
|
|
|
|
clean = false;
|
|
}
|
|
|
|
// remove all matches that lie after the current match
|
|
while ( m_pattern.length() < previousPattern.length() )
|
|
{
|
|
d->incrementalPath.remove(previousPattern);
|
|
previousPattern.truncate(previousPattern.length() - 1);
|
|
}
|
|
|
|
// set the current text, index, etc. to the found match
|
|
m_text = d->data[match->dataId]->text;
|
|
m_index = match->index;
|
|
m_matchedLength = match->matchedLength;
|
|
d->currentId = match->dataId;
|
|
|
|
// if the result is clean we can return it now
|
|
if ( clean )
|
|
{
|
|
if ( d->customIds )
|
|
emit highlight(d->currentId, m_index, m_matchedLength);
|
|
else
|
|
emit highlight(m_text, m_index, m_matchedLength);
|
|
|
|
m_lastResult = Match;
|
|
d->matchedPattern = m_pattern;
|
|
return Match;
|
|
}
|
|
}
|
|
// if we couldn't look up the match, the new pattern isn't a
|
|
// substring of the matchedPattern, so we start a new search
|
|
else
|
|
{
|
|
startNewIncrementalSearch();
|
|
}
|
|
}
|
|
// if the new pattern is longer than the matchedPattern we might be
|
|
// able to proceed from the last search
|
|
else if ( m_pattern.length() > d->matchedPattern.length() )
|
|
{
|
|
// continue from the previous pattern
|
|
if ( m_pattern.startsWith(d->matchedPattern) )
|
|
{
|
|
// we can't proceed from the previous position if the previous
|
|
// position already failed
|
|
if ( m_index == INDEX_NOMATCH )
|
|
return NoMatch;
|
|
|
|
QString temp = m_pattern;
|
|
m_pattern.truncate(d->matchedPattern.length() + 1);
|
|
d->matchedPattern = temp;
|
|
}
|
|
// start a new search
|
|
else
|
|
{
|
|
startNewIncrementalSearch();
|
|
}
|
|
}
|
|
// if the new pattern is as long as the matchedPattern, we reset if
|
|
// they are not equal
|
|
else if ( m_pattern != d->matchedPattern )
|
|
{
|
|
startNewIncrementalSearch();
|
|
}
|
|
}
|
|
|
|
#ifdef DEBUG_FIND
|
|
kdDebug() << k_funcinfo << "m_index=" << m_index << endl;
|
|
#endif
|
|
do
|
|
{
|
|
// if we have multiple data blocks in our cache, walk through these
|
|
// blocks till we either searched all blocks or we find a match
|
|
do
|
|
{
|
|
// Find the next candidate match.
|
|
if ( m_options & KFindDialog::RegularExpression )
|
|
m_index = KFind::find(m_text, *m_regExp, m_index, m_options, &m_matchedLength);
|
|
else
|
|
m_index = KFind::find(m_text, m_pattern, m_index, m_options, &m_matchedLength);
|
|
|
|
if ( m_options & KFindDialog::FindIncremental )
|
|
d->data[d->currentId]->dirty = false;
|
|
|
|
if ( m_index == -1 && d->currentId < (int) d->data.count() - 1 )
|
|
{
|
|
m_text = d->data[++d->currentId]->text;
|
|
|
|
if ( m_options & KFindDialog::FindBackwards )
|
|
m_index = m_text.length();
|
|
else
|
|
m_index = 0;
|
|
}
|
|
else
|
|
break;
|
|
} while ( !(m_options & KFindDialog::RegularExpression) );
|
|
|
|
if ( m_index != -1 )
|
|
{
|
|
// Flexibility: the app can add more rules to validate a possible match
|
|
if ( validateMatch( m_text, m_index, m_matchedLength ) )
|
|
{
|
|
bool done = true;
|
|
|
|
if ( m_options & KFindDialog::FindIncremental )
|
|
{
|
|
if ( m_pattern.isEmpty() ) {
|
|
delete d->emptyMatch;
|
|
d->emptyMatch = new Private::Match( d->currentId, m_index, m_matchedLength );
|
|
} else
|
|
d->incrementalPath.replace(m_pattern, new Private::Match(d->currentId, m_index, m_matchedLength));
|
|
|
|
if ( m_pattern.length() < d->matchedPattern.length() )
|
|
{
|
|
m_pattern += d->matchedPattern.mid(m_pattern.length(), 1);
|
|
done = false;
|
|
}
|
|
}
|
|
|
|
if ( done )
|
|
{
|
|
m_matches++;
|
|
// Tell the world about the match we found, in case someone wants to
|
|
// highlight it.
|
|
if ( d->customIds )
|
|
emit highlight(d->currentId, m_index, m_matchedLength);
|
|
else
|
|
emit highlight(m_text, m_index, m_matchedLength);
|
|
|
|
if ( !m_dialogClosed )
|
|
findNextDialog(true)->show();
|
|
|
|
#ifdef DEBUG_FIND
|
|
kdDebug() << k_funcinfo << "Match. Next m_index=" << m_index << endl;
|
|
#endif
|
|
m_lastResult = Match;
|
|
return Match;
|
|
}
|
|
}
|
|
else // Skip match
|
|
{
|
|
if (m_options & KFindDialog::FindBackwards)
|
|
m_index--;
|
|
else
|
|
m_index++;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if ( m_options & KFindDialog::FindIncremental )
|
|
{
|
|
QString temp = m_pattern;
|
|
temp.truncate(temp.length() - 1);
|
|
m_pattern = d->matchedPattern;
|
|
d->matchedPattern = temp;
|
|
}
|
|
|
|
m_index = INDEX_NOMATCH;
|
|
}
|
|
}
|
|
while (m_index != INDEX_NOMATCH);
|
|
|
|
#ifdef DEBUG_FIND
|
|
kdDebug() << k_funcinfo << "NoMatch. m_index=" << m_index << endl;
|
|
#endif
|
|
m_lastResult = NoMatch;
|
|
return NoMatch;
|
|
}
|
|
|
|
void KFind::startNewIncrementalSearch()
|
|
{
|
|
Private::Match *match = d->emptyMatch;
|
|
if(match == 0)
|
|
{
|
|
m_text = QString::null;
|
|
m_index = 0;
|
|
d->currentId = 0;
|
|
}
|
|
else
|
|
{
|
|
m_text = d->data[match->dataId]->text;
|
|
m_index = match->index;
|
|
d->currentId = match->dataId;
|
|
}
|
|
m_matchedLength = 0;
|
|
d->incrementalPath.clear();
|
|
delete d->emptyMatch;
|
|
d->emptyMatch = 0;
|
|
d->matchedPattern = m_pattern;
|
|
m_pattern = QString::null;
|
|
}
|
|
|
|
// static
|
|
int KFind::find(const QString &text, const QString &pattern, int index, long options, int *matchedLength)
|
|
{
|
|
// Handle regular expressions in the appropriate way.
|
|
if (options & KFindDialog::RegularExpression)
|
|
{
|
|
QRegExp regExp(pattern, options & KFindDialog::CaseSensitive);
|
|
|
|
return find(text, regExp, index, options, matchedLength);
|
|
}
|
|
|
|
bool caseSensitive = (options & KFindDialog::CaseSensitive);
|
|
|
|
if (options & KFindDialog::WholeWordsOnly)
|
|
{
|
|
if (options & KFindDialog::FindBackwards)
|
|
{
|
|
// Backward search, until the beginning of the line...
|
|
while (index >= 0)
|
|
{
|
|
// ...find the next match.
|
|
index = text.findRev(pattern, index, caseSensitive);
|
|
if (index == -1)
|
|
break;
|
|
|
|
// Is the match delimited correctly?
|
|
*matchedLength = pattern.length();
|
|
if (isWholeWords(text, index, *matchedLength))
|
|
break;
|
|
index--;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Forward search, until the end of the line...
|
|
while (index < (int)text.length())
|
|
{
|
|
// ...find the next match.
|
|
index = text.find(pattern, index, caseSensitive);
|
|
if (index == -1)
|
|
break;
|
|
|
|
// Is the match delimited correctly?
|
|
*matchedLength = pattern.length();
|
|
if (isWholeWords(text, index, *matchedLength))
|
|
break;
|
|
index++;
|
|
}
|
|
if (index >= (int)text.length()) // end of line
|
|
index = -1; // not found
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Non-whole-word search.
|
|
if (options & KFindDialog::FindBackwards)
|
|
{
|
|
index = text.findRev(pattern, index, caseSensitive);
|
|
}
|
|
else
|
|
{
|
|
index = text.find(pattern, index, caseSensitive);
|
|
}
|
|
if (index != -1)
|
|
{
|
|
*matchedLength = pattern.length();
|
|
}
|
|
}
|
|
return index;
|
|
}
|
|
|
|
// static
|
|
int KFind::find(const QString &text, const QRegExp &pattern, int index, long options, int *matchedLength)
|
|
{
|
|
if (options & KFindDialog::WholeWordsOnly)
|
|
{
|
|
if (options & KFindDialog::FindBackwards)
|
|
{
|
|
// Backward search, until the beginning of the line...
|
|
while (index >= 0)
|
|
{
|
|
// ...find the next match.
|
|
index = text.findRev(pattern, index);
|
|
if (index == -1)
|
|
break;
|
|
|
|
// Is the match delimited correctly?
|
|
//pattern.match(text, index, matchedLength, false);
|
|
/*int pos =*/ pattern.search( text.mid(index) );
|
|
*matchedLength = pattern.matchedLength();
|
|
if (isWholeWords(text, index, *matchedLength))
|
|
break;
|
|
index--;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Forward search, until the end of the line...
|
|
while (index < (int)text.length())
|
|
{
|
|
// ...find the next match.
|
|
index = text.find(pattern, index);
|
|
if (index == -1)
|
|
break;
|
|
|
|
// Is the match delimited correctly?
|
|
//pattern.match(text, index, matchedLength, false);
|
|
/*int pos =*/ pattern.search( text.mid(index) );
|
|
*matchedLength = pattern.matchedLength();
|
|
if (isWholeWords(text, index, *matchedLength))
|
|
break;
|
|
index++;
|
|
}
|
|
if (index >= (int)text.length()) // end of line
|
|
index = -1; // not found
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Non-whole-word search.
|
|
if (options & KFindDialog::FindBackwards)
|
|
{
|
|
index = text.findRev(pattern, index);
|
|
}
|
|
else
|
|
{
|
|
index = text.find(pattern, index);
|
|
}
|
|
if (index != -1)
|
|
{
|
|
//pattern.match(text, index, matchedLength, false);
|
|
/*int pos =*/ pattern.search( text.mid(index) );
|
|
*matchedLength = pattern.matchedLength();
|
|
}
|
|
}
|
|
return index;
|
|
}
|
|
|
|
bool KFind::isInWord(QChar ch)
|
|
{
|
|
return ch.isLetter() || ch.isDigit() || ch == '_';
|
|
}
|
|
|
|
bool KFind::isWholeWords(const QString &text, int starts, int matchedLength)
|
|
{
|
|
if ((starts == 0) || (!isInWord(text[starts - 1])))
|
|
{
|
|
int ends = starts + matchedLength;
|
|
|
|
if ((ends == (int)text.length()) || (!isInWord(text[ends])))
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void KFind::slotFindNext()
|
|
{
|
|
emit findNext();
|
|
}
|
|
|
|
void KFind::slotDialogClosed()
|
|
{
|
|
emit dialogClosed();
|
|
m_dialogClosed = true;
|
|
}
|
|
|
|
void KFind::displayFinalDialog() const
|
|
{
|
|
QString message;
|
|
if ( numMatches() )
|
|
message = i18n( "1 match found.", "%n matches found.", numMatches() );
|
|
else
|
|
message = i18n("<qt>No matches found for '<b>%1</b>'.</qt>").arg(QStyleSheet::escape(m_pattern));
|
|
KMessageBox::information(dialogsParent(), message);
|
|
}
|
|
|
|
bool KFind::shouldRestart( bool forceAsking, bool showNumMatches ) const
|
|
{
|
|
// Only ask if we did a "find from cursor", otherwise it's pointless.
|
|
// Well, unless the user can modify the document during a search operation,
|
|
// hence the force boolean.
|
|
if ( !forceAsking && (m_options & KFindDialog::FromCursor) == 0 )
|
|
{
|
|
displayFinalDialog();
|
|
return false;
|
|
}
|
|
QString message;
|
|
if ( showNumMatches )
|
|
{
|
|
if ( numMatches() )
|
|
message = i18n( "1 match found.", "%n matches found.", numMatches() );
|
|
else
|
|
message = i18n("No matches found for '<b>%1</b>'.").arg(QStyleSheet::escape(m_pattern));
|
|
}
|
|
else
|
|
{
|
|
if ( m_options & KFindDialog::FindBackwards )
|
|
message = i18n( "Beginning of document reached." );
|
|
else
|
|
message = i18n( "End of document reached." );
|
|
}
|
|
|
|
message += "<br><br>"; // can't be in the i18n() of the first if() because of the plural form.
|
|
// Hope this word puzzle is ok, it's a different sentence
|
|
message +=
|
|
( m_options & KFindDialog::FindBackwards ) ?
|
|
i18n("Continue from the end?")
|
|
: i18n("Continue from the beginning?");
|
|
|
|
int ret = KMessageBox::questionYesNo( dialogsParent(), QString("<qt>")+message+QString("</qt>"),
|
|
QString::null, KStdGuiItem::cont(), KStdGuiItem::stop() );
|
|
bool yes = ( ret == KMessageBox::Yes );
|
|
if ( yes )
|
|
const_cast<KFind*>(this)->m_options &= ~KFindDialog::FromCursor; // clear FromCursor option
|
|
return yes;
|
|
}
|
|
|
|
void KFind::setOptions( long options )
|
|
{
|
|
m_options = options;
|
|
|
|
delete m_regExp;
|
|
if (m_options & KFindDialog::RegularExpression)
|
|
m_regExp = new QRegExp(m_pattern, m_options & KFindDialog::CaseSensitive);
|
|
else
|
|
m_regExp = 0;
|
|
}
|
|
|
|
void KFind::closeFindNextDialog()
|
|
{
|
|
delete m_dialog;
|
|
m_dialog = 0L;
|
|
m_dialogClosed = true;
|
|
}
|
|
|
|
int KFind::index() const
|
|
{
|
|
return m_index;
|
|
}
|
|
|
|
void KFind::setPattern( const QString& pattern )
|
|
{
|
|
if ( m_options & KFindDialog::FindIncremental && m_pattern != pattern )
|
|
d->patternChanged = true;
|
|
|
|
m_pattern = pattern;
|
|
setOptions( options() ); // rebuild m_regExp if necessary
|
|
}
|
|
|
|
QWidget* KFind::dialogsParent() const
|
|
{
|
|
// If the find dialog is still up, it should get the focus when closing a message box
|
|
// Otherwise, maybe the "find next?" dialog is up
|
|
// Otherwise, the "view" is the parent.
|
|
return d->findDialog ? (QWidget*)d->findDialog : ( m_dialog ? m_dialog : parentWidget() );
|
|
}
|
|
|
|
#include "kfind.moc"
|