1 // This module implements the QsciAPIs class.
2 //
3 // Copyright (c) 2021 Riverbank Computing Limited <info@riverbankcomputing.com>
4 //
5 // This file is part of QScintilla.
6 //
7 // This file may be used under the terms of the GNU General Public License
8 // version 3.0 as published by the Free Software Foundation and appearing in
9 // the file LICENSE included in the packaging of this file.  Please review the
10 // following information to ensure the GNU General Public License version 3.0
11 // requirements will be met: http://www.gnu.org/copyleft/gpl.html.
12 //
13 // If you do not wish to use this file under the terms of the GPL version 3.0
14 // then you may purchase a commercial license.  For more information contact
15 // info@riverbankcomputing.com.
16 //
17 // This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE
18 // WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
19 
20 
21 #include <stdlib.h>
22 
23 #include <algorithm>
24 
25 #include "Qsci/qsciapis.h"
26 
27 #include <QApplication>
28 #include <QDataStream>
29 #include <QDir>
30 #include <QEvent>
31 #include <QFile>
32 #include <QLibraryInfo>
33 #include <QMap>
34 #include <QTextStream>
35 #include <QThread>
36 
37 #include "Qsci/qscilexer.h"
38 
39 
40 
41 // The version number of the prepared API information format.
42 const unsigned char PreparedDataFormatVersion = 0;
43 
44 
45 // This class contains prepared API information.
46 struct QsciAPIsPrepared
47 {
48     // The word dictionary is a map of individual words and a list of positions
49     // each occurs in the sorted list of APIs.  A position is a tuple of the
50     // index into the list of APIs and the index into the particular API.
51     QMap<QString, QsciAPIs::WordIndexList> wdict;
52 
53     // The case dictionary maps the case insensitive words to the form in which
54     // they are to be used.  It is only used if the language is case
55     // insensitive.
56     QMap<QString, QString> cdict;
57 
58     // The raw API information.
59     QStringList raw_apis;
60 
61     QStringList apiWords(int api_idx, const QStringList &wseps,
62             bool strip_image) const;
63     static QString apiBaseName(const QString &api);
64 };
65 
66 
67 // Return a particular API entry as a list of words.
apiWords(int api_idx,const QStringList & wseps,bool strip_image) const68 QStringList QsciAPIsPrepared::apiWords(int api_idx, const QStringList &wseps,
69         bool strip_image) const
70 {
71     QString base = apiBaseName(raw_apis[api_idx]);
72 
73     // Remove any embedded image reference if necessary.
74     if (strip_image)
75     {
76         int tail = base.indexOf('?');
77 
78         if (tail >= 0)
79             base.truncate(tail);
80     }
81 
82     if (wseps.isEmpty())
83         return QStringList(base);
84 
85     return base.split(wseps.first());
86 }
87 
88 
89 // Return the name of an API function, ie. without the arguments.
apiBaseName(const QString & api)90 QString QsciAPIsPrepared::apiBaseName(const QString &api)
91 {
92     QString base = api;
93     int tail = base.indexOf('(');
94 
95     if (tail >= 0)
96         base.truncate(tail);
97 
98     return base.simplified();
99 }
100 
101 
102 // The user event type that signals that the worker thread has started.
103 const QEvent::Type WorkerStarted = static_cast<QEvent::Type>(QEvent::User + 1012);
104 
105 
106 // The user event type that signals that the worker thread has finished.
107 const QEvent::Type WorkerFinished = static_cast<QEvent::Type>(QEvent::User + 1013);
108 
109 
110 // The user event type that signals that the worker thread has aborted.
111 const QEvent::Type WorkerAborted = static_cast<QEvent::Type>(QEvent::User + 1014);
112 
113 
114 // This class is the worker thread that post-processes the API set.
115 class QsciAPIsWorker : public QThread
116 {
117 public:
118     QsciAPIsWorker(QsciAPIs *apis);
119     virtual ~QsciAPIsWorker();
120 
121     virtual void run();
122 
123     QsciAPIsPrepared *prepared;
124 
125 private:
126     QsciAPIs *proxy;
127     bool abort;
128 };
129 
130 
131 // The worker thread ctor.
QsciAPIsWorker(QsciAPIs * apis)132 QsciAPIsWorker::QsciAPIsWorker(QsciAPIs *apis)
133     : prepared(0), proxy(apis), abort(false)
134 {
135 }
136 
137 
138 // The worker thread dtor.
~QsciAPIsWorker()139 QsciAPIsWorker::~QsciAPIsWorker()
140 {
141     // Tell the thread to stop.  There is no need to bother with a mutex.
142     abort = true;
143 
144     // Wait for it to do so and hit it if it doesn't.
145     if (!wait(500))
146         terminate();
147 
148     if (prepared)
149         delete prepared;
150 }
151 
152 
153 // The worker thread entry point.
run()154 void QsciAPIsWorker::run()
155 {
156     // Sanity check.
157     if (!prepared)
158         return;
159 
160     // Tell the main thread we have started.
161     QApplication::postEvent(proxy, new QEvent(WorkerStarted));
162 
163     // Sort the full list.
164     prepared->raw_apis.sort();
165 
166     QStringList wseps = proxy->lexer()->autoCompletionWordSeparators();
167     bool cs = proxy->lexer()->caseSensitive();
168 
169     // Split each entry into separate words but ignoring any arguments.
170     for (int a = 0; a < prepared->raw_apis.count(); ++a)
171     {
172         // Check to see if we should stop.
173         if (abort)
174             break;
175 
176         QStringList words = prepared->apiWords(a, wseps, true);
177 
178         for (int w = 0; w < words.count(); ++w)
179         {
180             const QString &word = words[w];
181 
182             // Add the word's position to any existing list for this word.
183             QsciAPIs::WordIndexList wil = prepared->wdict[word];
184 
185             // If the language is case insensitive and we haven't seen this
186             // word before then save it in the case dictionary.
187             if (!cs && wil.count() == 0)
188                 prepared->cdict[word.toUpper()] = word;
189 
190             wil.append(QsciAPIs::WordIndex(a, w));
191             prepared->wdict[word] = wil;
192         }
193     }
194 
195     // Tell the main thread we have finished.
196     QApplication::postEvent(proxy, new QEvent(abort ? WorkerAborted : WorkerFinished));
197 }
198 
199 
200 // The ctor.
QsciAPIs(QsciLexer * lexer)201 QsciAPIs::QsciAPIs(QsciLexer *lexer)
202     : QsciAbstractAPIs(lexer), worker(0), origin_len(0)
203 {
204     prep = new QsciAPIsPrepared;
205 }
206 
207 
208 // The dtor.
~QsciAPIs()209 QsciAPIs::~QsciAPIs()
210 {
211     deleteWorker();
212     delete prep;
213 }
214 
215 
216 // Delete the worker thread if there is one.
deleteWorker()217 void QsciAPIs::deleteWorker()
218 {
219     if (worker)
220     {
221         delete worker;
222         worker = 0;
223     }
224 }
225 
226 
227 //! Handle termination events from the worker thread.
event(QEvent * e)228 bool QsciAPIs::event(QEvent *e)
229 {
230     switch (e->type())
231     {
232     case WorkerStarted:
233         emit apiPreparationStarted();
234         return true;
235 
236     case WorkerAborted:
237         deleteWorker();
238         emit apiPreparationCancelled();
239         return true;
240 
241     case WorkerFinished:
242         delete prep;
243         old_context.clear();
244 
245         prep = worker->prepared;
246         worker->prepared = 0;
247         deleteWorker();
248 
249         // Allow the raw API information to be modified.
250         apis = prep->raw_apis;
251 
252         emit apiPreparationFinished();
253 
254         return true;
255 
256     default:
257         break;
258     }
259 
260     return QObject::event(e);
261 }
262 
263 
264 // Clear the current raw API entries.
clear()265 void QsciAPIs::clear()
266 {
267     apis.clear();
268 }
269 
270 
271 // Clear out all API information.
load(const QString & filename)272 bool QsciAPIs::load(const QString &filename)
273 {
274     QFile f(filename);
275 
276     if (!f.open(QIODevice::ReadOnly))
277         return false;
278 
279     QTextStream ts(&f);
280 
281     for (;;)
282     {
283         QString line = ts.readLine();
284 
285         if (line.isEmpty())
286             break;
287 
288         apis.append(line);
289     }
290 
291     return true;
292 }
293 
294 
295 // Add a single API entry.
add(const QString & entry)296 void QsciAPIs::add(const QString &entry)
297 {
298     apis.append(entry);
299 }
300 
301 
302 // Remove a single API entry.
remove(const QString & entry)303 void QsciAPIs::remove(const QString &entry)
304 {
305     int idx = apis.indexOf(entry);
306 
307     if (idx >= 0)
308         apis.removeAt(idx);
309 }
310 
311 
312 // Position the "origin" cursor into the API entries according to the user
313 // supplied context.
positionOrigin(const QStringList & context,QString & path)314 QStringList QsciAPIs::positionOrigin(const QStringList &context, QString &path)
315 {
316     // Get the list of words and see if the context is the same as last time we
317     // were called.
318     QStringList new_context;
319     bool same_context = (old_context.count() > 0 && old_context.count() < context.count());
320 
321     for (int i = 0; i < context.count(); ++i)
322     {
323         QString word = context[i];
324 
325         if (!lexer()->caseSensitive())
326             word = word.toUpper();
327 
328         if (i < old_context.count() && old_context[i] != word)
329             same_context = false;
330 
331         new_context << word;
332     }
333 
334     // If the context has changed then reset the origin.
335     if (!same_context)
336         origin_len = 0;
337 
338     // If we have a current origin (ie. the user made a specific selection in
339     // the current context) then adjust the origin to include the last complete
340     // word as the user may have entered more parts of the name without using
341     // auto-completion.
342     if (origin_len > 0)
343     {
344         const QString wsep = lexer()->autoCompletionWordSeparators().first();
345 
346         int start_new = old_context.count();
347         int end_new = new_context.count() - 1;
348 
349         if (start_new == end_new)
350         {
351             path = old_context.join(wsep);
352             origin_len = path.length();
353         }
354         else
355         {
356             QString fixed = *origin;
357             fixed.truncate(origin_len);
358 
359             path = fixed;
360 
361             while (start_new < end_new)
362             {
363                 // Add this word to the current path.
364                 path.append(wsep);
365                 path.append(new_context[start_new]);
366                 origin_len = path.length();
367 
368                 // Skip entries in the current origin that don't match the
369                 // path.
370                 while (origin != prep->raw_apis.end())
371                 {
372                     // See if the current origin has come to an end.
373                     if (!originStartsWith(fixed, wsep))
374                         origin = prep->raw_apis.end();
375                     else if (originStartsWith(path, wsep))
376                         break;
377                     else
378                         ++origin;
379                 }
380 
381                 if (origin == prep->raw_apis.end())
382                     break;
383 
384                 ++start_new;
385             }
386         }
387 
388         // Terminate the path.
389         path.append(wsep);
390 
391         // If the new text wasn't recognised then reset the origin.
392         if (origin == prep->raw_apis.end())
393             origin_len = 0;
394     }
395 
396     if (origin_len == 0)
397         path.truncate(0);
398 
399     // Save the "committed" context for next time.
400     old_context = new_context;
401     old_context.removeLast();
402 
403     return new_context;
404 }
405 
406 
407 // Return true if the origin starts with the given path.
originStartsWith(const QString & path,const QString & wsep)408 bool QsciAPIs::originStartsWith(const QString &path, const QString &wsep)
409 {
410     const QString &orig = *origin;
411 
412     if (!orig.startsWith(path))
413         return false;
414 
415     // Check that the path corresponds to the end of a word, ie. that what
416     // follows in the origin is either a word separator or a (.
417     QString tail = orig.mid(path.length());
418 
419     return (!tail.isEmpty() && (tail.startsWith(wsep) || tail.at(0) == '('));
420 }
421 
422 
423 // Add auto-completion words to an existing list.
updateAutoCompletionList(const QStringList & context,QStringList & list)424 void QsciAPIs::updateAutoCompletionList(const QStringList &context,
425         QStringList &list)
426 {
427     QString path;
428     QStringList new_context = positionOrigin(context, path);
429 
430     if (origin_len > 0)
431     {
432         const QString wsep = lexer()->autoCompletionWordSeparators().first();
433         QStringList::const_iterator it = origin;
434 
435         unambiguous_context = path;
436 
437         while (it != prep->raw_apis.end())
438         {
439             QString base = QsciAPIsPrepared::apiBaseName(*it);
440 
441             if (!base.startsWith(path))
442                 break;
443 
444             // Make sure we have something after the path.
445             if (base != path)
446             {
447                 // Get the word we are interested in (ie. the one after the
448                 // current origin in path).
449                 QString w = base.mid(origin_len + wsep.length()).split(wsep).first();
450 
451                 // Append the space, we know the origin is unambiguous.
452                 w.append(' ');
453 
454                 if (!list.contains(w))
455                     list << w;
456             }
457 
458             ++it;
459         }
460     }
461     else
462     {
463         // At the moment we assume we will add words from multiple contexts so
464         // mark the unambiguous context as unknown.
465         unambiguous_context = QString();
466 
467         bool unambig = true;
468         QStringList with_context;
469 
470         if (new_context.last().isEmpty())
471             lastCompleteWord(new_context[new_context.count() - 2], with_context, unambig);
472         else
473             lastPartialWord(new_context.last(), with_context, unambig);
474 
475         for (int i = 0; i < with_context.count(); ++i)
476         {
477             // Remove any unambigious context (allowing for a possible image
478             // identifier).
479             QString noc = with_context[i];
480 
481             if (unambig)
482             {
483                 int op = noc.indexOf(QLatin1String(" ("));
484 
485                 if (op >= 0)
486                 {
487                     int cl = noc.indexOf(QLatin1String(")"));
488 
489                     if (cl > op)
490                         noc.remove(op, cl - op + 1);
491                     else
492                         noc.truncate(op);
493                 }
494             }
495 
496             list << noc;
497         }
498     }
499 }
500 
501 
502 // Get the index list for a particular word if there is one.
wordIndexOf(const QString & word) const503 const QsciAPIs::WordIndexList *QsciAPIs::wordIndexOf(const QString &word) const
504 {
505     QString csword;
506 
507     // Indirect through the case dictionary if the language isn't case
508     // sensitive.
509     if (lexer()->caseSensitive())
510         csword = word;
511     else
512     {
513         csword = prep->cdict[word];
514 
515         if (csword.isEmpty())
516             return 0;
517     }
518 
519     // Get the possible API entries if any.
520     const WordIndexList *wl = &prep->wdict[csword];
521 
522     if (wl->isEmpty())
523         return 0;
524 
525     return wl;
526 }
527 
528 
529 // Add auto-completion words based on the last complete word entered.
lastCompleteWord(const QString & word,QStringList & with_context,bool & unambig)530 void QsciAPIs::lastCompleteWord(const QString &word, QStringList &with_context, bool &unambig)
531 {
532     // Get the possible API entries if any.
533     const WordIndexList *wl = wordIndexOf(word);
534 
535     if (wl)
536         addAPIEntries(*wl, true, with_context, unambig);
537 }
538 
539 
540 // Add auto-completion words based on the last partial word entered.
lastPartialWord(const QString & word,QStringList & with_context,bool & unambig)541 void QsciAPIs::lastPartialWord(const QString &word, QStringList &with_context, bool &unambig)
542 {
543     if (lexer()->caseSensitive())
544     {
545         QMap<QString, WordIndexList>::const_iterator it = prep->wdict.lowerBound(word);
546 
547         while (it != prep->wdict.end())
548         {
549             if (!it.key().startsWith(word))
550                 break;
551 
552             addAPIEntries(it.value(), false, with_context, unambig);
553 
554             ++it;
555         }
556     }
557     else
558     {
559         QMap<QString, QString>::const_iterator it = prep->cdict.lowerBound(word);
560 
561         while (it != prep->cdict.end())
562         {
563             if (!it.key().startsWith(word))
564                 break;
565 
566             addAPIEntries(prep->wdict[it.value()], false, with_context, unambig);
567 
568             ++it;
569         }
570     }
571 }
572 
573 
574 // Handle the selection of an entry in the auto-completion list.
autoCompletionSelected(const QString & selection)575 void QsciAPIs::autoCompletionSelected(const QString &selection)
576 {
577     // If the selection is an API (ie. it has a space separating the selected
578     // word and the optional origin) then remember the origin.
579     QStringList lst = selection.split(' ');
580 
581     if (lst.count() != 2)
582     {
583         origin_len = 0;
584         return;
585     }
586 
587     const QString &path = lst[1];
588     QString owords;
589 
590     if (path.isEmpty())
591         owords = unambiguous_context;
592     else
593     {
594         // Check the parenthesis.
595         if (!path.startsWith("(") || !path.endsWith(")"))
596         {
597             origin_len = 0;
598             return;
599         }
600 
601         // Remove the parenthesis.
602         owords = path.mid(1, path.length() - 2);
603     }
604 
605     origin = std::lower_bound(prep->raw_apis.begin(), prep->raw_apis.end(),
606             owords);
607     origin_len = owords.length();
608 }
609 
610 
611 // Add auto-completion words for a particular word (defined by where it appears
612 // in the APIs) and depending on whether the word was complete (when it's
613 // actually the next word in the API entry that is of interest) or not.
addAPIEntries(const WordIndexList & wl,bool complete,QStringList & with_context,bool & unambig)614 void QsciAPIs::addAPIEntries(const WordIndexList &wl, bool complete,
615         QStringList &with_context, bool &unambig)
616 {
617     QStringList wseps = lexer()->autoCompletionWordSeparators();
618 
619     for (int w = 0; w < wl.count(); ++w)
620     {
621         const WordIndex &wi = wl[w];
622 
623         QStringList api_words = prep->apiWords(wi.first, wseps, false);
624 
625         int idx = wi.second;
626 
627         if (complete)
628         {
629             // Skip if this is the last word.
630             if (++idx >= api_words.count())
631                 continue;
632         }
633 
634         QString api_word, org;
635 
636         if (idx == 0)
637         {
638             api_word = api_words[0] + ' ';
639             org = QString::fromLatin1("");
640         }
641         else
642         {
643             QStringList orgl = api_words.mid(0, idx);
644             org = orgl.join(wseps.first());
645 
646             // Add the context (allowing for a possible image identifier).
647             QString w = api_words[idx];
648             QString type;
649             int type_idx = w.indexOf(QLatin1String("?"));
650 
651             if (type_idx >= 0)
652             {
653                 type = w.mid(type_idx);
654                 w.truncate(type_idx);
655             }
656 
657             api_word = QString("%1 (%2)%3").arg(w).arg(org).arg(type);
658         }
659 
660         // If the origin is different to the context then the context is
661         // ambiguous.
662         if (unambig)
663         {
664             if (unambiguous_context.isNull())
665             {
666                 unambiguous_context = org;
667             }
668             else if (unambiguous_context != org)
669             {
670                 unambiguous_context.truncate(0);
671                 unambig = false;
672             }
673         }
674 
675         if (!with_context.contains(api_word))
676             with_context.append(api_word);
677     }
678 }
679 
680 
681 // Return the call tip for a function.
callTips(const QStringList & context,int commas,QsciScintilla::CallTipsStyle style,QList<int> & shifts)682 QStringList QsciAPIs::callTips(const QStringList &context, int commas,
683         QsciScintilla::CallTipsStyle style, QList<int> &shifts)
684 {
685     QString path;
686     QStringList new_context = positionOrigin(context, path);
687     QStringList wseps = lexer()->autoCompletionWordSeparators();
688     QStringList cts;
689 
690     if (origin_len > 0)
691     {
692         // The path should have a trailing word separator.
693         const QString &wsep = wseps.first();
694         path.chop(wsep.length());
695 
696         QStringList::const_iterator it = origin;
697         QString prev;
698 
699         // Work out the length of the context.
700         QStringList strip = path.split(wsep);
701         strip.removeLast();
702         int ctstart = strip.join(wsep).length();
703 
704         if (ctstart)
705             ctstart += wsep.length();
706 
707         int shift;
708 
709         if (style == QsciScintilla::CallTipsContext)
710         {
711             shift = ctstart;
712             ctstart = 0;
713         }
714         else
715             shift = 0;
716 
717         // Make sure we only look at the functions we are interested in.
718         path.append('(');
719 
720         while (it != prep->raw_apis.end() && (*it).startsWith(path))
721         {
722             QString w = (*it).mid(ctstart);
723 
724             if (w != prev && enoughCommas(w, commas))
725             {
726                 shifts << shift;
727                 cts << w;
728                 prev = w;
729             }
730 
731             ++it;
732         }
733     }
734     else
735     {
736         const QString &fname = new_context[new_context.count() - 2];
737 
738         // Find everywhere the function name appears in the APIs.
739         const WordIndexList *wil = wordIndexOf(fname);
740 
741         if (wil)
742             for (int i = 0; i < wil->count(); ++i)
743             {
744                 const WordIndex &wi = (*wil)[i];
745                 QStringList awords = prep->apiWords(wi.first, wseps, true);
746 
747                 // Check the word is the function name and not part of any
748                 // context.
749                 if (wi.second != awords.count() - 1)
750                     continue;
751 
752                 const QString &api = prep->raw_apis[wi.first];
753 
754                 int tail = api.indexOf('(');
755 
756                 if (tail < 0)
757                     continue;
758 
759                 if (!enoughCommas(api, commas))
760                     continue;
761 
762                 if (style == QsciScintilla::CallTipsNoContext)
763                 {
764                     shifts << 0;
765                     cts << (fname + api.mid(tail));
766                 }
767                 else
768                 {
769                     shifts << tail - fname.length();
770 
771                     // Remove any image type.
772                     int im_type = api.indexOf('?');
773 
774                     if (im_type <= 0)
775                         cts << api;
776                     else
777                         cts << (api.left(im_type - 1) + api.mid(tail));
778                 }
779             }
780     }
781 
782     return cts;
783 }
784 
785 
786 // Return true if a string has enough commas in the argument list.
enoughCommas(const QString & s,int commas)787 bool QsciAPIs::enoughCommas(const QString &s, int commas)
788 {
789     int end = s.indexOf(')');
790 
791     if (end < 0)
792         return false;
793 
794     QString w = s.left(end);
795 
796     return (w.count(',') >= commas);
797 }
798 
799 
800 // Ensure the list is ready.
prepare()801 void QsciAPIs::prepare()
802 {
803     // Handle the trivial case.
804     if (worker)
805         return;
806 
807     QsciAPIsPrepared *new_apis = new QsciAPIsPrepared;
808     new_apis->raw_apis = apis;
809 
810     worker = new QsciAPIsWorker(this);
811     worker->prepared = new_apis;
812     worker->start();
813 }
814 
815 
816 // Cancel any current preparation.
cancelPreparation()817 void QsciAPIs::cancelPreparation()
818 {
819     deleteWorker();
820 }
821 
822 
823 // Check that a prepared API file exists.
isPrepared(const QString & filename) const824 bool QsciAPIs::isPrepared(const QString &filename) const
825 {
826     QString pname = prepName(filename);
827 
828     if (pname.isEmpty())
829         return false;
830 
831     QFileInfo fi(pname);
832 
833     return fi.exists();
834 }
835 
836 
837 // Load the prepared API information.
loadPrepared(const QString & filename)838 bool QsciAPIs::loadPrepared(const QString &filename)
839 {
840     QString pname = prepName(filename);
841 
842     if (pname.isEmpty())
843         return false;
844 
845     // Read the prepared data and decompress it.
846     QFile pf(pname);
847 
848     if (!pf.open(QIODevice::ReadOnly))
849         return false;
850 
851     QByteArray cpdata = pf.readAll();
852 
853     pf.close();
854 
855     if (cpdata.count() == 0)
856         return false;
857 
858     QByteArray pdata = qUncompress(cpdata);
859 
860     // Extract the data.
861     QDataStream pds(pdata);
862 
863     unsigned char vers;
864     pds >> vers;
865 
866     if (vers > PreparedDataFormatVersion)
867         return false;
868 
869     char *lex_name;
870     pds >> lex_name;
871 
872     if (qstrcmp(lex_name, lexer()->lexer()) != 0)
873     {
874         delete[] lex_name;
875         return false;
876     }
877 
878     delete[] lex_name;
879 
880     prep->wdict.clear();
881     pds >> prep->wdict;
882 
883     if (!lexer()->caseSensitive())
884     {
885         // Build up the case dictionary.
886         prep->cdict.clear();
887 
888         QMap<QString, WordIndexList>::const_iterator it = prep->wdict.begin();
889 
890         while (it != prep->wdict.end())
891         {
892             prep->cdict[it.key().toUpper()] = it.key();
893             ++it;
894         }
895     }
896 
897     prep->raw_apis.clear();
898     pds >> prep->raw_apis;
899 
900     // Allow the raw API information to be modified.
901     apis = prep->raw_apis;
902 
903     return true;
904 }
905 
906 
907 // Save the prepared API information.
savePrepared(const QString & filename) const908 bool QsciAPIs::savePrepared(const QString &filename) const
909 {
910     QString pname = prepName(filename, true);
911 
912     if (pname.isEmpty())
913         return false;
914 
915     // Write the prepared data to a memory buffer.
916     QByteArray pdata;
917     QDataStream pds(&pdata, QIODevice::WriteOnly);
918 
919     // Use a serialisation format supported by Qt v3.0 and later.
920     pds.setVersion(QDataStream::Qt_3_0);
921     pds << PreparedDataFormatVersion;
922     pds << lexer()->lexer();
923     pds << prep->wdict;
924     pds << prep->raw_apis;
925 
926     // Compress the data and write it.
927     QFile pf(pname);
928 
929     if (!pf.open(QIODevice::WriteOnly|QIODevice::Truncate))
930         return false;
931 
932     if (pf.write(qCompress(pdata)) < 0)
933     {
934         pf.close();
935         return false;
936     }
937 
938     pf.close();
939     return true;
940 }
941 
942 
943 // Return the name of the default prepared API file.
defaultPreparedName() const944 QString QsciAPIs::defaultPreparedName() const
945 {
946     return prepName(QString());
947 }
948 
949 
950 // Return the name of a prepared API file.
prepName(const QString & filename,bool mkpath) const951 QString QsciAPIs::prepName(const QString &filename, bool mkpath) const
952 {
953     // Handle the tivial case.
954     if (!filename.isEmpty())
955         return filename;
956 
957     QString pdname;
958     char *qsci = getenv("QSCIDIR");
959 
960     if (qsci)
961         pdname = qsci;
962     else
963     {
964         static const char *qsci_dir = ".qsci";
965 
966         QDir pd = QDir::home();
967 
968         if (mkpath && !pd.exists(qsci_dir) && !pd.mkdir(qsci_dir))
969             return QString();
970 
971         pdname = pd.filePath(qsci_dir);
972     }
973 
974     return QString("%1/%2.pap").arg(pdname).arg(lexer()->lexer());
975 }
976 
977 
978 // Return installed API files.
installedAPIFiles() const979 QStringList QsciAPIs::installedAPIFiles() const
980 {
981     QString qtdir = QLibraryInfo::location(QLibraryInfo::DataPath);
982 
983     QDir apidir = QDir(QString("%1/qsci/api/%2").arg(qtdir).arg(lexer()->lexer()));
984     QStringList filenames;
985 
986     QStringList filters;
987     filters << "*.api";
988 
989     QFileInfoList flist = apidir.entryInfoList(filters, QDir::Files, QDir::IgnoreCase);
990 
991     foreach (QFileInfo fi, flist)
992         filenames << fi.absoluteFilePath();
993 
994     return filenames;
995 }
996