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