// This module implements the QsciAPIs class. // // Copyright (c) 2007 // Riverbank Computing Limited // // This file is part of QScintilla. // // This copy of QScintilla 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, or (at your option) any // later version. // // QScintilla is supplied 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 // QScintilla; see the file LICENSE. If not, write to the Free Software // Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. #include #include "Qsci/qsciapis.h" #include #include #include #include #include #include #include #include #include #include "Qsci/qscilexer.h" // The version number of the prepared API information format. const unsigned char PreparedDataFormatVersion = 0; // This class contains prepared API information. struct QsciAPIsPrepared { // The word dictionary is a map of individual words and a list of positions // each occurs in the sorted list of APIs. A position is a tuple of the // index into the list of APIs and the index into the particular API. QMap wdict; // The case dictionary maps the case insensitive words to the form in which // they are to be used. It is only used if the language is case // insensitive. QMap cdict; // The raw API information. QStringList raw_apis; QStringList apiWords(int api_idx, const QStringList &wseps) const; static QString apiBaseName(const QString &api); }; // Return a particular API entry as a list of words. QStringList QsciAPIsPrepared::apiWords(int api_idx, const QStringList &wseps) const { QString base = apiBaseName(raw_apis[api_idx]); if (wseps.isEmpty()) return QStringList(base); return base.split(wseps.first()); } // Return the name of an API function, ie. without the arguments. QString QsciAPIsPrepared::apiBaseName(const QString &api) { QString base = api; int tail = base.indexOf('('); if (tail >= 0) base.truncate(tail); return base; } // The user event type that signals that the worker thread has started. const QEvent::Type WorkerStarted = static_cast(QEvent::User + 1012); // The user event type that signals that the worker thread has finished. const QEvent::Type WorkerFinished = static_cast(QEvent::User + 1013); // The user event type that signals that the worker thread has aborted. const QEvent::Type WorkerAborted = static_cast(QEvent::User + 1014); // This class is the worker thread that post-processes the API set. class QsciAPIsWorker : public QThread { public: QsciAPIsWorker(QsciAPIs *apis); virtual ~QsciAPIsWorker(); virtual void run(); QsciAPIsPrepared *prepared; private: QsciAPIs *proxy; bool abort; }; // The worker thread ctor. QsciAPIsWorker::QsciAPIsWorker(QsciAPIs *apis) : proxy(apis), prepared(0), abort(false) { } // The worker thread dtor. QsciAPIsWorker::~QsciAPIsWorker() { // Tell the thread to stop. There is no need to bother with a mutex. abort = true; // Wait for it to do so and hit it if it doesn't. if (!wait(500)) terminate(); if (prepared) delete prepared; } // The worker thread entry point. void QsciAPIsWorker::run() { // Sanity check. if (!prepared) return; // Tell the main thread we have started. QApplication::postEvent(proxy, new QEvent(WorkerStarted)); // Sort the full list. prepared->raw_apis.sort(); QStringList wseps = proxy->lex->autoCompletionWordSeparators(); bool cs = proxy->lex->caseSensitive(); // Split each entry into separate words but ignoring any arguments. for (int a = 0; a < prepared->raw_apis.count(); ++a) { // Check to see if we should stop. if (abort) break; QStringList words = prepared->apiWords(a, wseps); for (int w = 0; w < words.count(); ++w) { const QString &word = words[w]; // Add the word's position to any existing list for this word. QsciAPIs::WordIndexList wil = prepared->wdict[word]; // If the language is case insensitive and we haven't seen this // word before then save it in the case dictionary. if (!cs && wil.count() == 0) prepared->cdict[word.toUpper()] = word; wil.append(QsciAPIs::WordIndex(a, w)); prepared->wdict[word] = wil; } } // Tell the main thread we have finished. QApplication::postEvent(proxy, new QEvent(abort ? WorkerAborted : WorkerFinished)); } // The ctor. QsciAPIs::QsciAPIs(QsciLexer *lexer) : QObject(lexer), lex(lexer), worker(0), ctcursor(0), origin_len(0) { prep = new QsciAPIsPrepared; lexer->setAPIs(this); } // The dtor. QsciAPIs::~QsciAPIs() { deleteWorker(); delete prep; } // Delete the worker thread if there is one. void QsciAPIs::deleteWorker() { if (worker) { delete worker; worker = 0; } } //! Handle termination events from the worker thread. bool QsciAPIs::event(QEvent *e) { switch (e->type()) { case WorkerStarted: emit apiPreparationStarted(); return true; case WorkerAborted: deleteWorker(); emit apiPreparationCancelled(); return true; case WorkerFinished: delete prep; old_context.clear(); prep = worker->prepared; worker->prepared = 0; deleteWorker(); // Allow the raw API information to be modified. apis = prep->raw_apis; emit apiPreparationFinished(); return true; } return QObject::event(e); } // Clear the current raw API entries. void QsciAPIs::clear() { apis.clear(); } // Clear out all API information. bool QsciAPIs::load(const QString &fname) { QFile f(fname); if (!f.open(QIODevice::ReadOnly)) return false; QTextStream ts(&f); for (;;) { QString line = ts.readLine(); if (line.isEmpty()) break; apis.append(line); } return true; } // Add a single API entry. void QsciAPIs::add(const QString &entry) { apis.append(entry); } // Remove a single API entry. void QsciAPIs::remove(const QString &entry) { int idx = apis.indexOf(entry); if (idx >= 0) apis.removeAt(idx); } // Position the "origin" cursor into the API entries according to the user // supplied context. QStringList QsciAPIs::positionOrigin(const QStringList &context, QString &path) { // Get the list of words and see if the context is the same as last time we // were called. QStringList new_context; bool same_context = (old_context.count() > 0 && old_context.count() < context.count()); for (int i = 0; i < context.count(); ++i) { QString word = context[i]; if (!lex->caseSensitive()) word = word.toUpper(); if (i < old_context.count() && old_context[i] != word) same_context = false; new_context << word; } // If the context has changed then reset the origin. if (!same_context) origin_len = 0; // If we have a current origin (ie. the user made a specific selection in // the current context) then adjust the origin to include the last complete // word as the user may have entered more parts of the name without using // auto-completion. if (origin_len > 0) { const QString wsep = lex->autoCompletionWordSeparators().first(); int start_new = old_context.count(); int end_new = new_context.count() - 1; QString fixed = *origin; fixed.truncate(origin_len); path = fixed; while (start_new < end_new) { // Add this word to the current path. path.append(wsep); path.append(new_context[start_new]); origin_len = path.length(); // Skip entries in the current origin that don't match the path. while (origin != prep->raw_apis.end()) { // See if the current origin has come to an end. if (!originStartsWith(fixed, wsep)) origin = prep->raw_apis.end(); else if (originStartsWith(path, wsep)) break; else ++origin; } if (origin == prep->raw_apis.end()) break; ++start_new; } // If the new text wasn't recognised then reset the origin. if (origin == prep->raw_apis.end()) origin_len = 0; } if (origin_len == 0) path.truncate(0); // Save the "committed" context for next time. old_context = new_context; old_context.removeLast(); return new_context; } // Return true if the origin starts with the given path. bool QsciAPIs::originStartsWith(const QString &path, const QString &wsep) { const QString &orig = *origin; if (!orig.startsWith(path)) return false; // Check that the path corresponds to the end of a word, ie. that what // follows in the origin is either a word separator or a (. QString tail = orig.mid(path.length()); return (tail.startsWith(wsep) || tail.at(0) == '('); } // Add auto-completion words to an existing list. void QsciAPIs::autoCompletionList(const QStringList &context, QStringList &wlist) { QString path; QStringList new_context = positionOrigin(context, path); if (origin_len > 0) { const QString wsep = lex->autoCompletionWordSeparators().first(); QStringList::const_iterator it = origin; unambiguous_context = path; while (it != prep->raw_apis.end()) { QString base = QsciAPIsPrepared::apiBaseName(*it); if (!base.startsWith(path)) break; // Make sure we have something after the path. if (base != path) { // Get the word we are interested in (ie. the one after the // current origin in path). QString w = base.mid(origin_len + wsep.length()).split(wsep).first(); // Append the space, we know the origin is unambiguous. w.append(' '); if (!wlist.contains(w)) wlist << w; } ++it; } } else { // At the moment we assume we will add words from multiple contexts. unambiguous_context.truncate(0); bool unambig = true; QStringList with_context; if (new_context.last().isEmpty()) lastCompleteWord(new_context[new_context.count() - 2], with_context, unambig); else lastPartialWord(new_context.last(), with_context, unambig); for (int i = 0; i < with_context.count(); ++i) { // Remove any unambigious context. QString noc = with_context[i]; if (unambig) { int op = noc.indexOf('('); if (op >= 0) noc.truncate(op); } wlist << noc; } } } // Get the index list for a particular word if there is one. const QsciAPIs::WordIndexList *QsciAPIs::wordIndexOf(const QString &word) const { QString csword; // Indirect through the case dictionary if the language isn't case // sensitive. if (lex->caseSensitive()) csword = word; else { csword = prep->cdict[word]; if (csword.isEmpty()) return 0; } // Get the possible API entries if any. const WordIndexList *wl = &prep->wdict[csword]; if (wl->isEmpty()) return 0; return wl; } // Add auto-completion words based on the last complete word entered. void QsciAPIs::lastCompleteWord(const QString &word, QStringList &with_context, bool &unambig) { // Get the possible API entries if any. const WordIndexList *wl = wordIndexOf(word); if (wl) addAPIEntries(*wl, true, with_context, unambig); } // Add auto-completion words based on the last partial word entered. void QsciAPIs::lastPartialWord(const QString &word, QStringList &with_context, bool &unambig) { if (lex->caseSensitive()) { QMap::const_iterator it = prep->wdict.lowerBound(word); while (it != prep->wdict.end()) { if (!it.key().startsWith(word)) break; addAPIEntries(it.value(), false, with_context, unambig); ++it; } } else { QMap::const_iterator it = prep->cdict.lowerBound(word); while (it != prep->cdict.end()) { if (!it.key().startsWith(word)) break; addAPIEntries(prep->wdict[it.value()], false, with_context, unambig); ++it; } } } // Handle the selection of an entry in the auto-completion list. void QsciAPIs::autoCompletionSelected(const QString &sel) { // If the selection is an API (ie. it has a space separating the selected // word and the optional origin) then remember the origin. QStringList lst = sel.split(' '); if (lst.count() != 2) { origin_len = 0; return; } const QString &path = lst[1]; QString owords; if (path.isEmpty()) owords = unambiguous_context; else { // Check the parenthesis. if (!path.startsWith("(") || !path.endsWith(")")) { origin_len = 0; return; } // Remove the parenthesis. owords = path.mid(1, path.length() - 2); } origin = qLowerBound(prep->raw_apis, owords); origin_len = owords.length(); } // Add auto-completion words for a particular word (defined by where it appears // in the APIs) and depending on whether the word was complete (when it's // actually the next word in the API entry that is of interest) or not. void QsciAPIs::addAPIEntries(const WordIndexList &wl, bool complete, QStringList &with_context, bool &unambig) { QStringList wseps = lex->autoCompletionWordSeparators(); for (int w = 0; w < wl.count(); ++w) { const WordIndex &wi = wl[w]; QStringList api_words = prep->apiWords(wi.first, wseps); int idx = wi.second; if (complete) { // Skip if this is the last word. if (++idx >= api_words.count()) continue; } QString api_word; if (idx == 0) api_word = api_words[0] + ' '; else { QStringList orgl = api_words.mid(0, idx); QString org = orgl.join(wseps.first()); api_word = QString("%1 (%2)").arg(api_words[idx]).arg(org); // See if the origin has been used before. if (unambig) if (unambiguous_context.isEmpty()) unambiguous_context = org; else if (unambiguous_context != org) { unambiguous_context.truncate(0); unambig = false; } } if (!with_context.contains(api_word)) with_context.append(api_word); } } // Return the call tip for a function. QString QsciAPIs::callTips(const QStringList &context, QsciScintilla::CallTipsStyle style, int maxnr, int commas, int &ctshift) { QString path; QStringList new_context = positionOrigin(context, path); QStringList wseps = lex->autoCompletionWordSeparators(); QStringList cts; if (origin_len > 0) { QStringList::const_iterator it = origin; QString prev; // Work out the lenght of the context. const QString &wsep = wseps.first(); QStringList strip = path.split(wsep); strip.removeLast(); int ctstart = strip.join(wsep).length(); if (ctstart) ctstart += wsep.length(); int shift; if (style == QsciScintilla::CallTipsContext) { shift = ctstart; ctstart = 0; } else shift = 0; // Make sure we only look at the functions we are interested in. path.append('('); while (it != prep->raw_apis.end() && (*it).startsWith(path)) { if (maxnr > 0 && maxnr == cts.count()) break; QString w = (*it).mid(ctstart); if (w != prev && enoughCommas(w, commas)) { ctshifts << shift; cts << w; prev = w; } ++it; } } else { const QString &fname = new_context[new_context.count() - 2]; // Find everywhere the function name appears in the APIs. const WordIndexList *wil = wordIndexOf(fname); if (wil) for (int i = 0; i < wil->count(); ++i) { const WordIndex &wi = (*wil)[i]; QStringList awords = prep->apiWords(wi.first, wseps); // Check the word is the function name and not part of any // context. if (wi.second != awords.count() - 1) continue; const QString &api = prep->raw_apis[wi.first]; int tail = api.indexOf('('); if (tail < 0) continue; if (!enoughCommas(api, commas)) continue; if (maxnr > 0 && maxnr == cts.count()) break; if (style == QsciScintilla::CallTipsNoContext) { ctshifts << 0; cts << (fname + api.mid(tail)); } else { ctshifts << tail - fname.length(); cts << api; } } } // See if we want to add a down arrow. if (maxnr < 0 && cts.count() > 1) { // Remember the state so we can scroll through it later. ctlist = cts; ctcursor = 0; QString ct = cts[0]; ct.prepend('\002'); ctshift = ctshifts.first(); return ct; } // Find the biggest shift. ctshift = 0; for (int i = 0; i < ctshifts.count(); ++i) { int shift = ctshifts[i]; if (ctshift < shift) ctshift = shift; } ctshifts.clear(); ctlist.clear(); return cts.join("\n"); } // Return the next or previous call tip. QString QsciAPIs::callTipsNextPrev(int dir, int &ctshift) { QString ct; // Get the call tip. if (dir == 1 && ctcursor > 0) ct = ctlist[--ctcursor]; else if (dir == 2 && ctcursor < ctlist.count() - 1) ct = ctlist[++ctcursor]; // Add the arrows. if (!ct.isEmpty()) { if (ctcursor < ctlist.count() - 1) ct.prepend('\002'); if (ctcursor > 0) ct.prepend('\001'); ctshift = ctshifts[ctcursor]; } return ct; } // Return true if a string has enough commas in the argument list. bool QsciAPIs::enoughCommas(const QString &s, int commas) { int end = s.indexOf(')'); if (end < 0) return false; QString w = s.left(end); return (w.count(',') >= commas); } // Ensure the list is ready. void QsciAPIs::prepare() { // Handle the trivial case. if (worker) return; QsciAPIsPrepared *new_apis = new QsciAPIsPrepared; new_apis->raw_apis = apis; worker = new QsciAPIsWorker(this); worker->prepared = new_apis; worker->start(); } // Cancel any current preparation. void QsciAPIs::cancelPreparation() { deleteWorker(); } // Check that a prepared API file exists. bool QsciAPIs::isPrepared(const QString &fname) const { QString pname = prepName(fname); if (pname.isEmpty()) return false; QFileInfo fi(pname); return fi.exists(); } // Load the prepared API information. bool QsciAPIs::loadPrepared(const QString &fname) { QString pname = prepName(fname); if (pname.isEmpty()) return false; // Read the prepared data and decompress it. QFile pf(pname); if (!pf.open(QIODevice::ReadOnly)) return false; QByteArray cpdata = pf.readAll(); pf.close(); if (cpdata.count() == 0) return false; QByteArray pdata = qUncompress(cpdata); // Extract the data. QDataStream pds(pdata); unsigned char vers; pds >> vers; if (vers > PreparedDataFormatVersion) return false; char *lex_name; pds >> lex_name; if (qstrcmp(lex_name, lex->lexer()) != 0) { delete[] lex_name; return false; } delete[] lex_name; prep->wdict.clear(); pds >> prep->wdict; if (!lex->caseSensitive()) { // Build up the case dictionary. prep->cdict.clear(); QMap::const_iterator it = prep->wdict.begin(); while (it != prep->wdict.end()) { prep->cdict[it.key().toUpper()] = it.key(); ++it; } } prep->raw_apis.clear(); pds >> prep->raw_apis; // Allow the raw API information to be modified. apis = prep->raw_apis; return true; } // Save the prepared API information. bool QsciAPIs::savePrepared(const QString &fname) const { QString pname = prepName(fname, true); if (pname.isEmpty()) return false; // Write the prepared data to a memory buffer. QByteArray pdata; QDataStream pds(&pdata, QIODevice::WriteOnly); // Use a serialisation format supported by Qt v3.0 and later. pds.setVersion(QDataStream::Qt_3_0); pds << PreparedDataFormatVersion; pds << lex->lexer(); pds << prep->wdict; pds << prep->raw_apis; // Compress the data and write it. QFile pf(pname); if (!pf.open(QIODevice::WriteOnly|QIODevice::Truncate)) return false; if (pf.write(qCompress(pdata)) < 0) { pf.close(); return false; } pf.close(); return true; } // Return the name of the default prepared API file. QString QsciAPIs::defaultPreparedName() const { return prepName(QString()); } // Return the name of a prepared API file. QString QsciAPIs::prepName(const QString &fname, bool mkpath) const { // Handle the tivial case. if (!fname.isEmpty()) return fname; QString pdname; char *qsci = getenv("QSCIDIR"); if (qsci) pdname = qsci; else { static const char *qsci_dir = ".qsci"; QDir pd = QDir::home(); if (mkpath && !pd.exists(qsci_dir) && !pd.mkdir(qsci_dir)) return QString(); pdname = pd.filePath(qsci_dir); } return QString("%1/%2.pap").arg(pdname).arg(lex->lexer()); } // Return installed API files. QStringList QsciAPIs::installedAPIFiles() const { QString qtdir = QLibraryInfo::location(QLibraryInfo::DataPath); QDir apidir = QDir(QString("%1/qsci/api/%2").arg(qtdir).arg(lex->lexer())); QStringList fnames; QStringList filters; filters << "*.api"; QFileInfoList flist = apidir.entryInfoList(filters, QDir::Files, QDir::IgnoreCase); foreach (QFileInfo fi, flist) fnames << fi.absoluteFilePath(); return fnames; }