1 /****************************************************************************
2 **
3 ** Copyright (C) 2018 The Qt Company Ltd.
4 ** Contact: https://www.qt.io/licensing/
5 **
6 ** This file is part of Qt Creator.
7 **
8 ** Commercial License Usage
9 ** Licensees holding valid commercial Qt licenses may use this file in
10 ** accordance with the commercial license agreement provided with the
11 ** Software or, alternatively, in accordance with the terms contained in
12 ** a written agreement between you and The Qt Company. For licensing terms
13 ** and conditions see https://www.qt.io/terms-conditions. For further
14 ** information use the contact form at https://www.qt.io/contact-us.
15 **
16 ** GNU General Public License Usage
17 ** Alternatively, this file may be used under the terms of the GNU
18 ** General Public License version 3 as published by the Free Software
19 ** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
20 ** included in the packaging of this file. Please review the following
21 ** information to ensure the GNU General Public License requirements will
22 ** be met: https://www.gnu.org/licenses/gpl-3.0.html.
23 **
24 ****************************************************************************/
25 
26 #include "documentcontentcompletion.h"
27 
28 #include "assistinterface.h"
29 #include "assistproposalitem.h"
30 #include "genericproposal.h"
31 #include "genericproposalmodel.h"
32 #include "iassistprocessor.h"
33 #include "../snippets/snippetassistcollector.h"
34 #include "../completionsettings.h"
35 #include "../texteditorsettings.h"
36 
37 #include <utils/algorithm.h>
38 #include <utils/runextensions.h>
39 
40 #include <QElapsedTimer>
41 #include <QRegularExpression>
42 #include <QSet>
43 #include <QTextBlock>
44 #include <QTextDocument>
45 
46 using namespace TextEditor;
47 
48 class DocumentContentCompletionProcessor final : public IAssistProcessor
49 {
50 public:
51     DocumentContentCompletionProcessor(const QString &snippetGroupId);
52     ~DocumentContentCompletionProcessor() final;
53 
54     IAssistProposal *perform(const AssistInterface *interface) override;
running()55     bool running() final { return m_watcher.isRunning(); }
56     void cancel() final;
57 
58 private:
59     QString m_snippetGroup;
60     QFutureWatcher<QStringList> m_watcher;
61 };
62 
DocumentContentCompletionProvider(const QString & snippetGroup)63 DocumentContentCompletionProvider::DocumentContentCompletionProvider(const QString &snippetGroup)
64     : m_snippetGroup(snippetGroup)
65 { }
66 
runType() const67 IAssistProvider::RunType DocumentContentCompletionProvider::runType() const
68 {
69     return Asynchronous;
70 }
71 
createProcessor() const72 IAssistProcessor *DocumentContentCompletionProvider::createProcessor() const
73 {
74     return new DocumentContentCompletionProcessor(m_snippetGroup);
75 }
76 
DocumentContentCompletionProcessor(const QString & snippetGroupId)77 DocumentContentCompletionProcessor::DocumentContentCompletionProcessor(const QString &snippetGroupId)
78     : m_snippetGroup(snippetGroupId)
79 { }
80 
~DocumentContentCompletionProcessor()81 DocumentContentCompletionProcessor::~DocumentContentCompletionProcessor()
82 {
83     cancel();
84 }
85 
createProposal(QFutureInterface<QStringList> & future,const QString & text,const QString & wordUnderCursor)86 static void createProposal(QFutureInterface<QStringList> &future, const QString &text,
87                            const QString &wordUnderCursor)
88 {
89     const QRegularExpression wordRE("([a-zA-Z_][a-zA-Z0-9_]{2,})");
90 
91     QSet<QString> words;
92     QRegularExpressionMatchIterator it = wordRE.globalMatch(text);
93     int wordUnderCursorFound = 0;
94     while (it.hasNext()) {
95         if (future.isCanceled())
96             return;
97         QRegularExpressionMatch match = it.next();
98         const QString &word = match.captured();
99         if (word == wordUnderCursor) {
100             // Only add the word under cursor if it
101             // already appears elsewhere in the text
102             if (++wordUnderCursorFound < 2)
103                 continue;
104         }
105 
106         if (!words.contains(word))
107             words.insert(word);
108     }
109 
110     future.reportResult(Utils::toList(words));
111 }
112 
perform(const AssistInterface * interface)113 IAssistProposal *DocumentContentCompletionProcessor::perform(const AssistInterface *interface)
114 {
115     QScopedPointer<const AssistInterface> assistInterface(interface);
116     if (running())
117         return nullptr;
118 
119     int pos = interface->position();
120 
121     QChar chr;
122     // Skip to the start of a name
123     do {
124         chr = interface->characterAt(--pos);
125     } while (chr.isLetterOrNumber() || chr == '_');
126 
127     ++pos;
128     int length = interface->position() - pos;
129 
130     if (interface->reason() == IdleEditor) {
131         QChar characterUnderCursor = interface->characterAt(interface->position());
132         if (characterUnderCursor.isLetterOrNumber()
133                 || length < TextEditorSettings::completionSettings().m_characterThreshold) {
134             return nullptr;
135         }
136     }
137 
138     const QString wordUnderCursor = interface->textAt(pos, length);
139     const QString text = interface->textDocument()->toPlainText();
140 
141     m_watcher.setFuture(Utils::runAsync(&createProposal, text, wordUnderCursor));
142     QObject::connect(&m_watcher, &QFutureWatcher<QStringList>::resultReadyAt,
143                      &m_watcher, [this, pos](int index){
144         const TextEditor::SnippetAssistCollector snippetCollector(
145                     m_snippetGroup, QIcon(":/texteditor/images/snippet.png"));
146         QList<AssistProposalItemInterface *> items = snippetCollector.collect();
147         for (const QString &word : m_watcher.resultAt(index)) {
148             auto item = new AssistProposalItem();
149             item->setText(word);
150             items.append(item);
151         }
152         setAsyncProposalAvailable(new GenericProposal(pos, items));
153     });
154     return nullptr;
155 }
156 
cancel()157 void DocumentContentCompletionProcessor::cancel()
158 {
159     if (running())
160         m_watcher.cancel();
161 }
162