1 /****************************************************************************
2 **
3 ** Copyright (C) 2016 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 "codeassistant.h"
27 #include "completionassistprovider.h"
28 #include "iassistprocessor.h"
29 #include "iassistproposal.h"
30 #include "iassistproposalmodel.h"
31 #include "iassistproposalwidget.h"
32 #include "assistinterface.h"
33 #include "assistproposalitem.h"
34 #include "runner.h"
35 #include "textdocumentmanipulator.h"
36 
37 #include <texteditor/textdocument.h>
38 #include <texteditor/texteditor.h>
39 #include <texteditor/texteditorsettings.h>
40 #include <texteditor/completionsettings.h>
41 #include <coreplugin/editormanager/editormanager.h>
42 #include <extensionsystem/pluginmanager.h>
43 #include <utils/algorithm.h>
44 #include <utils/qtcassert.h>
45 
46 #include <QKeyEvent>
47 #include <QList>
48 #include <QObject>
49 #include <QScopedPointer>
50 #include <QTimer>
51 
52 using namespace TextEditor::Internal;
53 
54 namespace TextEditor {
55 
56 class CodeAssistantPrivate : public QObject
57 {
58 public:
59     CodeAssistantPrivate(CodeAssistant *assistant);
60 
61     void configure(TextEditorWidget *editorWidget);
62     bool isConfigured() const;
63 
64     void invoke(AssistKind kind, IAssistProvider *provider = nullptr);
65     void process();
66     void requestProposal(AssistReason reason, AssistKind kind, IAssistProvider *provider = nullptr);
67     void cancelCurrentRequest();
68     void invalidateCurrentRequestData();
69     void displayProposal(IAssistProposal *newProposal, AssistReason reason);
70     bool isDisplayingProposal() const;
71     bool isWaitingForProposal() const;
72 
73     void notifyChange();
74     bool hasContext() const;
75     void destroyContext();
76 
77     QVariant userData() const;
78     void setUserData(const QVariant &data);
79 
80     CompletionAssistProvider *identifyActivationSequence();
81 
82     void stopAutomaticProposalTimer();
83     void startAutomaticProposalTimer();
84     void automaticProposalTimeout();
85     void clearAbortedPosition();
86     void updateFromCompletionSettings(const TextEditor::CompletionSettings &settings);
87 
88     bool eventFilter(QObject *o, QEvent *e) override;
89 
90 private:
91     bool requestActivationCharProposal();
92     void processProposalItem(AssistProposalItemInterface *proposalItem);
93     void handlePrefixExpansion(const QString &newPrefix);
94     void finalizeProposal();
95     void explicitlyAborted();
96     bool isDestroyEvent(int key, const QString &keyText);
97 
98 private:
99     CodeAssistant *q = nullptr;
100     TextEditorWidget *m_editorWidget = nullptr;
101     Internal::ProcessorRunner *m_requestRunner = nullptr;
102     QMetaObject::Connection m_runnerConnection;
103     IAssistProvider *m_requestProvider = nullptr;
104     IAssistProcessor *m_asyncProcessor = nullptr;
105     AssistKind m_assistKind = TextEditor::Completion;
106     IAssistProposalWidget *m_proposalWidget = nullptr;
107     QScopedPointer<IAssistProposal> m_proposal;
108     bool m_receivedContentWhileWaiting = false;
109     QTimer m_automaticProposalTimer;
110     CompletionSettings m_settings;
111     int m_abortedBasePosition = -1;
112     static const QChar m_null;
113     QVariant m_userData;
114 };
115 
116 // --------------------
117 // CodeAssistantPrivate
118 // --------------------
119 const QChar CodeAssistantPrivate::m_null;
120 
CodeAssistantPrivate(CodeAssistant * assistant)121 CodeAssistantPrivate::CodeAssistantPrivate(CodeAssistant *assistant)
122     : q(assistant)
123 {
124     m_automaticProposalTimer.setSingleShot(true);
125     connect(&m_automaticProposalTimer, &QTimer::timeout,
126             this, &CodeAssistantPrivate::automaticProposalTimeout);
127 
128     m_settings = TextEditorSettings::completionSettings();
129     connect(TextEditorSettings::instance(), &TextEditorSettings::completionSettingsChanged,
130             this, &CodeAssistantPrivate::updateFromCompletionSettings);
131 
132     connect(Core::EditorManager::instance(), &Core::EditorManager::currentEditorChanged,
133             this, &CodeAssistantPrivate::clearAbortedPosition);
134 }
135 
configure(TextEditorWidget * editorWidget)136 void CodeAssistantPrivate::configure(TextEditorWidget *editorWidget)
137 {
138     m_editorWidget = editorWidget;
139     m_editorWidget->installEventFilter(this);
140 }
141 
isConfigured() const142 bool CodeAssistantPrivate::isConfigured() const
143 {
144     return m_editorWidget != nullptr;
145 }
146 
invoke(AssistKind kind,IAssistProvider * provider)147 void CodeAssistantPrivate::invoke(AssistKind kind, IAssistProvider *provider)
148 {
149     if (!isConfigured())
150         return;
151 
152     stopAutomaticProposalTimer();
153 
154     if (isDisplayingProposal() && m_assistKind == kind && !m_proposal->isFragile()) {
155         m_proposalWidget->setReason(ExplicitlyInvoked);
156         m_proposalWidget->updateProposal(m_editorWidget->textAt(
157                         m_proposal->basePosition(),
158                         m_editorWidget->position() - m_proposal->basePosition()));
159     } else {
160         destroyContext();
161         requestProposal(ExplicitlyInvoked, kind, provider);
162     }
163 }
164 
requestActivationCharProposal()165 bool CodeAssistantPrivate::requestActivationCharProposal()
166 {
167     if (m_assistKind == Completion && m_settings.m_completionTrigger != ManualCompletion) {
168         if (CompletionAssistProvider *provider = identifyActivationSequence()) {
169             if (isWaitingForProposal())
170                 cancelCurrentRequest();
171             requestProposal(ActivationCharacter, Completion, provider);
172             return true;
173         }
174     }
175     return false;
176 }
177 
process()178 void CodeAssistantPrivate::process()
179 {
180     if (!isConfigured())
181         return;
182 
183     stopAutomaticProposalTimer();
184 
185     if (m_assistKind == TextEditor::Completion) {
186         if (!requestActivationCharProposal())
187             startAutomaticProposalTimer();
188     } else if (m_assistKind != FunctionHint){
189         m_assistKind = TextEditor::Completion;
190     }
191 }
192 
requestProposal(AssistReason reason,AssistKind kind,IAssistProvider * provider)193 void CodeAssistantPrivate::requestProposal(AssistReason reason,
194                                            AssistKind kind,
195                                            IAssistProvider *provider)
196 {
197     QTC_ASSERT(!isWaitingForProposal(), return);
198 
199     if (m_editorWidget->hasBlockSelection())
200         return; // TODO
201 
202     if (!provider) {
203         if (kind == Completion)
204             provider = m_editorWidget->textDocument()->completionAssistProvider();
205         else if (kind == FunctionHint)
206             provider = m_editorWidget->textDocument()->functionHintAssistProvider();
207         else
208             provider = m_editorWidget->textDocument()->quickFixAssistProvider();
209 
210         if (!provider)
211             return;
212     }
213 
214     AssistInterface *assistInterface = m_editorWidget->createAssistInterface(kind, reason);
215     if (!assistInterface)
216         return;
217 
218     m_assistKind = kind;
219     m_requestProvider = provider;
220     IAssistProcessor *processor = provider->createProcessor();
221 
222     switch (provider->runType()) {
223     case IAssistProvider::Synchronous: {
224         if (IAssistProposal *newProposal = processor->perform(assistInterface))
225             displayProposal(newProposal, reason);
226         delete processor;
227         break;
228     }
229     case IAssistProvider::AsynchronousWithThread: {
230         if (IAssistProposal *newProposal = processor->immediateProposal(assistInterface))
231             displayProposal(newProposal, reason);
232 
233         m_requestRunner = new ProcessorRunner;
234         m_runnerConnection = connect(m_requestRunner, &ProcessorRunner::finished,
235                                      this, [this, reason](){
236             // Since the request runner is a different thread, there's still a gap in which the
237             // queued signal could be processed after an invalidation of the current request.
238             if (!m_requestRunner || m_requestRunner != sender())
239                 return;
240 
241             IAssistProposal *proposal = m_requestRunner->proposal();
242             invalidateCurrentRequestData();
243             displayProposal(proposal, reason);
244             emit q->finished();
245         });
246         connect(m_requestRunner, &ProcessorRunner::finished,
247                 m_requestRunner, &ProcessorRunner::deleteLater);
248         assistInterface->prepareForAsyncUse();
249         m_requestRunner->setProcessor(processor);
250         m_requestRunner->setAssistInterface(assistInterface);
251         m_requestRunner->start();
252         break;
253     }
254     case IAssistProvider::Asynchronous: {
255         processor->setAsyncCompletionAvailableHandler([this, reason, processor](
256                 IAssistProposal *newProposal) {
257             if (!processor->running()) {
258                 // do not delete this processor directly since this function is called from within the processor
259                 QMetaObject::invokeMethod(QCoreApplication::instance(), [processor]() {
260                     delete processor;
261                 }, Qt::QueuedConnection);
262             }
263             if (processor != m_asyncProcessor)
264                 return;
265             invalidateCurrentRequestData();
266             if (processor && processor->needsRestart() && m_receivedContentWhileWaiting) {
267                 delete newProposal;
268                 m_receivedContentWhileWaiting = false;
269                 requestProposal(reason, m_assistKind, m_requestProvider);
270             } else {
271                 displayProposal(newProposal, reason);
272                 if (processor && processor->running())
273                     m_asyncProcessor = processor;
274                 else
275                     emit q->finished();
276             }
277         });
278 
279         // If there is a proposal, nothing asynchronous happened...
280         if (IAssistProposal *newProposal = processor->perform(assistInterface)) {
281             displayProposal(newProposal, reason);
282             delete processor;
283         } else if (!processor->running()) {
284             delete processor;
285         } else { // ...async request was triggered
286             if (IAssistProposal *newProposal = processor->immediateProposal(assistInterface))
287                 displayProposal(newProposal, reason);
288             QTC_CHECK(!m_asyncProcessor);
289             m_asyncProcessor = processor;
290         }
291 
292         break;
293     }
294     } // switch
295 }
296 
cancelCurrentRequest()297 void CodeAssistantPrivate::cancelCurrentRequest()
298 {
299     if (m_requestRunner) {
300         m_requestRunner->setDiscardProposal(true);
301         disconnect(m_runnerConnection);
302     }
303     if (m_asyncProcessor) {
304         m_asyncProcessor->cancel();
305         delete m_asyncProcessor;
306     }
307     invalidateCurrentRequestData();
308 }
309 
displayProposal(IAssistProposal * newProposal,AssistReason reason)310 void CodeAssistantPrivate::displayProposal(IAssistProposal *newProposal, AssistReason reason)
311 {
312     if (!newProposal)
313         return;
314 
315     // TODO: The proposal should own the model until someone takes it explicitly away.
316     QScopedPointer<IAssistProposal> proposalCandidate(newProposal);
317 
318     if (isDisplayingProposal() && !m_proposal->isFragile())
319         return;
320 
321     int basePosition = proposalCandidate->basePosition();
322     if (m_editorWidget->position() < basePosition) {
323         destroyContext();
324         return;
325     }
326 
327     if (m_abortedBasePosition == basePosition && reason != ExplicitlyInvoked) {
328         destroyContext();
329         return;
330     }
331 
332     const QString prefix = m_editorWidget->textAt(basePosition,
333                                                   m_editorWidget->position() - basePosition);
334     if (!newProposal->hasItemsToPropose(prefix, reason)) {
335         if (newProposal->isCorrective(m_editorWidget))
336             newProposal->makeCorrection(m_editorWidget);
337         return;
338     }
339 
340     destroyContext();
341 
342     clearAbortedPosition();
343     m_proposal.reset(proposalCandidate.take());
344 
345     if (m_proposal->isCorrective(m_editorWidget))
346         m_proposal->makeCorrection(m_editorWidget);
347 
348     m_editorWidget->keepAutoCompletionHighlight(true);
349     basePosition = m_proposal->basePosition();
350     m_proposalWidget = m_proposal->createWidget();
351     connect(m_proposalWidget, &QObject::destroyed,
352             this, &CodeAssistantPrivate::finalizeProposal);
353     connect(m_proposalWidget, &IAssistProposalWidget::prefixExpanded,
354             this, &CodeAssistantPrivate::handlePrefixExpansion);
355     connect(m_proposalWidget, &IAssistProposalWidget::proposalItemActivated,
356             this, &CodeAssistantPrivate::processProposalItem);
357     connect(m_proposalWidget, &IAssistProposalWidget::explicitlyAborted,
358             this, &CodeAssistantPrivate::explicitlyAborted);
359     m_proposalWidget->setAssistant(q);
360     m_proposalWidget->setReason(reason);
361     m_proposalWidget->setKind(m_assistKind);
362     m_proposalWidget->setBasePosition(basePosition);
363     m_proposalWidget->setUnderlyingWidget(m_editorWidget);
364     m_proposalWidget->setModel(m_proposal->model());
365     m_proposalWidget->setDisplayRect(m_editorWidget->cursorRect(basePosition));
366     m_proposalWidget->setIsSynchronized(!m_receivedContentWhileWaiting);
367     m_proposalWidget->showProposal(prefix);
368 }
369 
processProposalItem(AssistProposalItemInterface * proposalItem)370 void CodeAssistantPrivate::processProposalItem(AssistProposalItemInterface *proposalItem)
371 {
372     QTC_ASSERT(m_proposal, return);
373     TextDocumentManipulator manipulator(m_editorWidget);
374     proposalItem->apply(manipulator, m_proposal->basePosition());
375     destroyContext();
376     m_editorWidget->encourageApply();
377     if (!proposalItem->isSnippet())
378         requestActivationCharProposal();
379 }
380 
handlePrefixExpansion(const QString & newPrefix)381 void CodeAssistantPrivate::handlePrefixExpansion(const QString &newPrefix)
382 {
383     QTC_ASSERT(m_proposal, return);
384 
385     QTextCursor cursor(m_editorWidget->document());
386     cursor.setPosition(m_proposal->basePosition());
387     cursor.movePosition(QTextCursor::EndOfWord);
388 
389     int currentPosition = m_editorWidget->position();
390     const QString textAfterCursor = m_editorWidget->textAt(currentPosition,
391                                                        cursor.position() - currentPosition);
392     if (!textAfterCursor.startsWith(newPrefix)) {
393         if (newPrefix.indexOf(textAfterCursor, currentPosition - m_proposal->basePosition()) >= 0)
394             currentPosition = cursor.position();
395         const QStringView prefixAddition = QStringView(newPrefix).mid(currentPosition
396                                                                       - m_proposal->basePosition());
397         // If remaining string starts with the prefix addition
398         if (textAfterCursor.startsWith(prefixAddition))
399             currentPosition += prefixAddition.size();
400     }
401 
402     m_editorWidget->setCursorPosition(m_proposal->basePosition());
403     m_editorWidget->replace(currentPosition - m_proposal->basePosition(), newPrefix);
404     notifyChange();
405 }
406 
finalizeProposal()407 void CodeAssistantPrivate::finalizeProposal()
408 {
409     stopAutomaticProposalTimer();
410     m_proposal.reset();
411     m_proposalWidget = nullptr;
412     if (m_receivedContentWhileWaiting)
413         m_receivedContentWhileWaiting = false;
414 }
415 
isDisplayingProposal() const416 bool CodeAssistantPrivate::isDisplayingProposal() const
417 {
418     return m_proposalWidget != nullptr && m_proposalWidget->proposalIsVisible();
419 }
420 
isWaitingForProposal() const421 bool CodeAssistantPrivate::isWaitingForProposal() const
422 {
423     return m_requestRunner != nullptr || m_asyncProcessor != nullptr;
424 }
425 
invalidateCurrentRequestData()426 void CodeAssistantPrivate::invalidateCurrentRequestData()
427 {
428     m_asyncProcessor = nullptr;
429     m_requestRunner = nullptr;
430     m_requestProvider = nullptr;
431 }
432 
identifyActivationSequence()433 CompletionAssistProvider *CodeAssistantPrivate::identifyActivationSequence()
434 {
435     auto checkActivationSequence = [this](CompletionAssistProvider *provider) {
436         if (!provider)
437             return false;
438         const int length = provider->activationCharSequenceLength();
439         if (!length)
440             return false;
441         QString sequence = m_editorWidget->textAt(m_editorWidget->position() - length, length);
442         // In pretty much all cases the sequence will have the appropriate length. Only in the
443         // case of typing the very first characters in the document for providers that request a
444         // length greater than 1 (currently only C++, which specifies 3), the sequence needs to
445         // be prepended so it has the expected length.
446         const int lengthDiff = length - sequence.length();
447         for (int j = 0; j < lengthDiff; ++j)
448             sequence.prepend(m_null);
449         return provider->isActivationCharSequence(sequence);
450     };
451 
452     auto provider = {
453         m_editorWidget->textDocument()->completionAssistProvider(),
454         m_editorWidget->textDocument()->functionHintAssistProvider()
455     };
456     return Utils::findOrDefault(provider, checkActivationSequence);
457 }
458 
notifyChange()459 void CodeAssistantPrivate::notifyChange()
460 {
461     stopAutomaticProposalTimer();
462 
463     if (isDisplayingProposal()) {
464         QTC_ASSERT(m_proposal, return);
465         if (m_editorWidget->position() < m_proposal->basePosition()) {
466             destroyContext();
467         } else if (m_proposal->supportsPrefix()) {
468             m_proposalWidget->updateProposal(
469                 m_editorWidget->textAt(m_proposal->basePosition(),
470                                      m_editorWidget->position() - m_proposal->basePosition()));
471             if (!isDisplayingProposal())
472                 requestActivationCharProposal();
473         } else {
474             destroyContext();
475             requestProposal(ExplicitlyInvoked, m_assistKind, m_requestProvider);
476         }
477     }
478 }
479 
hasContext() const480 bool CodeAssistantPrivate::hasContext() const
481 {
482     return m_requestRunner || m_asyncProcessor || m_proposalWidget;
483 }
484 
destroyContext()485 void CodeAssistantPrivate::destroyContext()
486 {
487     stopAutomaticProposalTimer();
488 
489     if (isWaitingForProposal()) {
490         cancelCurrentRequest();
491     } else if (m_proposalWidget) {
492         m_editorWidget->keepAutoCompletionHighlight(false);
493         if (m_proposalWidget->proposalIsVisible())
494             m_proposalWidget->closeProposal();
495         disconnect(m_proposalWidget, &QObject::destroyed,
496                    this, &CodeAssistantPrivate::finalizeProposal);
497         finalizeProposal();
498     }
499 }
500 
userData() const501 QVariant CodeAssistantPrivate::userData() const
502 {
503     return m_userData;
504 }
505 
setUserData(const QVariant & data)506 void CodeAssistantPrivate::setUserData(const QVariant &data)
507 {
508     m_userData = data;
509 }
510 
startAutomaticProposalTimer()511 void CodeAssistantPrivate::startAutomaticProposalTimer()
512 {
513     if (m_settings.m_completionTrigger == AutomaticCompletion)
514         m_automaticProposalTimer.start();
515 }
516 
automaticProposalTimeout()517 void CodeAssistantPrivate::automaticProposalTimeout()
518 {
519     if (isWaitingForProposal() || (isDisplayingProposal() && !m_proposal->isFragile()))
520         return;
521 
522     requestProposal(IdleEditor, Completion);
523 }
524 
stopAutomaticProposalTimer()525 void CodeAssistantPrivate::stopAutomaticProposalTimer()
526 {
527     if (m_automaticProposalTimer.isActive())
528         m_automaticProposalTimer.stop();
529 }
530 
updateFromCompletionSettings(const TextEditor::CompletionSettings & settings)531 void CodeAssistantPrivate::updateFromCompletionSettings(
532         const TextEditor::CompletionSettings &settings)
533 {
534     m_settings = settings;
535     m_automaticProposalTimer.setInterval(m_settings.m_automaticProposalTimeoutInMs);
536 }
537 
explicitlyAborted()538 void CodeAssistantPrivate::explicitlyAborted()
539 {
540     QTC_ASSERT(m_proposal, return);
541     m_abortedBasePosition = m_proposal->basePosition();
542 }
543 
clearAbortedPosition()544 void CodeAssistantPrivate::clearAbortedPosition()
545 {
546     m_abortedBasePosition = -1;
547 }
548 
isDestroyEvent(int key,const QString & keyText)549 bool CodeAssistantPrivate::isDestroyEvent(int key, const QString &keyText)
550 {
551     if (keyText.isEmpty())
552         return key != Qt::LeftArrow && key != Qt::RightArrow && key != Qt::Key_Shift;
553     if (auto provider = qobject_cast<CompletionAssistProvider *>(m_requestProvider))
554         return !provider->isContinuationChar(keyText.at(0));
555     return false;
556 }
557 
eventFilter(QObject * o,QEvent * e)558 bool CodeAssistantPrivate::eventFilter(QObject *o, QEvent *e)
559 {
560     Q_UNUSED(o)
561 
562     if (isWaitingForProposal()) {
563         QEvent::Type type = e->type();
564         if (type == QEvent::FocusOut) {
565             destroyContext();
566         } else if (type == QEvent::KeyPress) {
567             auto keyEvent = static_cast<QKeyEvent *>(e);
568             const QString &keyText = keyEvent->text();
569 
570             if (isDestroyEvent(keyEvent->key(), keyText))
571                 destroyContext();
572             else if (!keyText.isEmpty() && !m_receivedContentWhileWaiting)
573                 m_receivedContentWhileWaiting = true;
574         } else if (type == QEvent::KeyRelease
575                    && static_cast<QKeyEvent *>(e)->key() == Qt::Key_Escape) {
576             destroyContext();
577         }
578     }
579 
580     return false;
581 }
582 
583 // -------------
584 // CodeAssistant
585 // -------------
CodeAssistant()586 CodeAssistant::CodeAssistant() : d(new CodeAssistantPrivate(this))
587 {
588 }
589 
~CodeAssistant()590 CodeAssistant::~CodeAssistant()
591 {
592     destroyContext();
593     delete d;
594 }
595 
configure(TextEditorWidget * editorWidget)596 void CodeAssistant::configure(TextEditorWidget *editorWidget)
597 {
598     d->configure(editorWidget);
599 }
600 
process()601 void CodeAssistant::process()
602 {
603     d->process();
604 }
605 
notifyChange()606 void CodeAssistant::notifyChange()
607 {
608     d->notifyChange();
609 }
610 
hasContext() const611 bool CodeAssistant::hasContext() const
612 {
613     return d->hasContext();
614 }
615 
destroyContext()616 void CodeAssistant::destroyContext()
617 {
618     d->destroyContext();
619 }
620 
userData() const621 QVariant CodeAssistant::userData() const
622 {
623     return d->userData();
624 }
625 
setUserData(const QVariant & data)626 void CodeAssistant::setUserData(const QVariant &data)
627 {
628     d->setUserData(data);
629 }
630 
invoke(AssistKind kind,IAssistProvider * provider)631 void CodeAssistant::invoke(AssistKind kind, IAssistProvider *provider)
632 {
633     d->invoke(kind, provider);
634 }
635 
636 } // namespace TextEditor
637