KTextEditor

katewordcompletion.cpp
1/*
2 SPDX-FileCopyrightText: 2003 Anders Lund <anders.lund@lund.tdcadsl.dk>
3 SPDX-FileCopyrightText: 2010 Christoph Cullmann <cullmann@kde.org>
4
5 SPDX-License-Identifier: LGPL-2.0-or-later
6*/
7
8// BEGIN includes
9#include "katewordcompletion.h"
10#include "kateconfig.h"
11#include "katedocument.h"
12#include "kateglobal.h"
13#include "kateview.h"
14
15#include <ktexteditor/movingrange.h>
16#include <ktexteditor/range.h>
17
18#include <KAboutData>
19#include <KActionCollection>
20#include <KConfigGroup>
21#include <KLocalizedString>
22#include <KPageDialog>
23#include <KPageWidgetModel>
24#include <KParts/Part>
25#include <KToggleAction>
26
27#include <Sonnet/Speller>
28
29#include <QAction>
30#include <QCheckBox>
31#include <QLabel>
32#include <QLayout>
33#include <QRegularExpression>
34#include <QSet>
35#include <QSpinBox>
36#include <QString>
37
38// END
39
40/// amount of lines to scan backwards and forwards
41static const int maxLinesToScan = 10000;
42
43// BEGIN KateWordCompletionModel
44KateWordCompletionModel::KateWordCompletionModel(QObject *parent)
45 : CodeCompletionModel(parent)
46 , m_automatic(false)
47{
48 setHasGroups(false);
49}
50
51KateWordCompletionModel::~KateWordCompletionModel()
52{
53}
54
55void KateWordCompletionModel::saveMatches(KTextEditor::View *view, const KTextEditor::Range &range)
56{
57 m_matches = allMatches(view, range);
58 m_matches.sort();
59}
60
61QVariant KateWordCompletionModel::data(const QModelIndex &index, int role) const
62{
63 if (role == UnimportantItemRole) {
64 return QVariant(true);
65 }
66 if (role == InheritanceDepth) {
67 return 10000;
68 }
69
70 if (!index.parent().isValid()) {
71 // It is the group header
72 switch (role) {
73 case Qt::DisplayRole:
74 return i18n("Auto Word Completion");
75 case GroupRole:
76 return Qt::DisplayRole;
77 }
78 }
79
80 if (index.column() == KTextEditor::CodeCompletionModel::Name && role == Qt::DisplayRole) {
81 return m_matches.at(index.row());
82 }
83
85 static QIcon icon(QIcon::fromTheme(QStringLiteral("insert-text")).pixmap(QSize(16, 16)));
86 return icon;
87 }
88
89 return QVariant();
90}
91
93{
94 if (index.internalId()) {
95 return createIndex(0, 0, quintptr(0));
96 } else {
97 return QModelIndex();
98 }
99}
100
101QModelIndex KateWordCompletionModel::index(int row, int column, const QModelIndex &parent) const
102{
103 if (!parent.isValid()) {
104 if (row == 0) {
105 return createIndex(row, column, quintptr(0));
106 } else {
107 return QModelIndex();
108 }
109
110 } else if (parent.parent().isValid()) {
111 return QModelIndex();
112 }
113
114 if (row < 0 || row >= m_matches.count() || column < 0 || column >= ColumnCount) {
115 return QModelIndex();
116 }
117
118 return createIndex(row, column, 1);
119}
120
121int KateWordCompletionModel::rowCount(const QModelIndex &parent) const
122{
123 if (!parent.isValid() && !m_matches.isEmpty()) {
124 return 1; // One root node to define the custom group
125 } else if (parent.parent().isValid()) {
126 return 0; // Completion-items have no children
127 } else {
128 return m_matches.count();
129 }
130}
131
132bool KateWordCompletionModel::shouldStartCompletion(KTextEditor::View *view,
133 const QString &insertedText,
134 bool userInsertion,
135 const KTextEditor::Cursor &position)
136{
137 if (!userInsertion) {
138 return false;
139 }
140 if (insertedText.isEmpty()) {
141 return false;
142 }
143
144 KTextEditor::ViewPrivate *v = qobject_cast<KTextEditor::ViewPrivate *>(view);
145
146 const QString &text = view->document()->line(position.line()).left(position.column());
147 const uint check = v->config()->wordCompletionMinimalWordLength();
148 // Start completion immediately if min. word size is zero
149 if (!check) {
150 return true;
151 }
152 // Otherwise, check if user has typed long enough text...
153 const int start = text.length();
154 const int end = start - check;
155 if (end < 0) {
156 return false;
157 }
158 for (int i = start - 1; i >= end; i--) {
159 const QChar c = text.at(i);
160 if (!(c.isLetter() || (c.isNumber()) || c == QLatin1Char('_'))) {
161 return false;
162 }
163 }
164
165 return true;
166}
167
168bool KateWordCompletionModel::shouldAbortCompletion(KTextEditor::View *view, const KTextEditor::Range &range, const QString &currentCompletion)
169{
170 if (m_automatic) {
171 KTextEditor::ViewPrivate *v = qobject_cast<KTextEditor::ViewPrivate *>(view);
172 if (currentCompletion.length() < v->config()->wordCompletionMinimalWordLength()) {
173 return true;
174 }
175 }
176
177 return CodeCompletionModelControllerInterface::shouldAbortCompletion(view, range, currentCompletion);
178}
179
180void KateWordCompletionModel::completionInvoked(KTextEditor::View *view, const KTextEditor::Range &range, InvocationType it)
181{
182 m_automatic = it == AutomaticInvocation;
183 saveMatches(view, range);
184}
185
186/**
187 * Scan throughout the entire document for possible completions,
188 * ignoring any dublets and words shorter than configured and/or
189 * reasonable minimum length.
190 */
191QStringList KateWordCompletionModel::allMatches(KTextEditor::View *view, const KTextEditor::Range &range)
192{
193 QSet<QStringView> result;
194 const int minWordSize = qMax(2, qobject_cast<KTextEditor::ViewPrivate *>(view)->config()->wordCompletionMinimalWordLength());
195 const auto cursorPosition = view->cursorPosition();
196 const auto document = view->document();
197 const int startLine = std::max(0, cursorPosition.line() - maxLinesToScan);
198 const int endLine = std::min(cursorPosition.line() + maxLinesToScan, view->document()->lines());
199 for (int line = startLine; line < endLine; line++) {
200 const QString text = document->line(line);
201 if (text.isEmpty() || text.isNull()) {
202 continue;
203 }
204 QStringView textView = text;
205 int wordBegin = 0;
206 int offset = 0;
207 const int end = text.size();
208 const bool cursorLine = cursorPosition.line() == line;
209 const bool isNotLastLine = line != range.end().line();
210 const QChar *d = text.data();
211 while (offset < end) {
212 const QChar c = d[offset];
213 // increment offset when at line end, so we take the last character too
214 if ((!c.isLetterOrNumber() && c != QChar(u'_')) || (offset == end - 1 && offset++)) {
215 if (offset - wordBegin >= minWordSize && (isNotLastLine || offset != range.end().column())) {
216 // don't add the word we are inside with cursor!
217 if (!cursorLine || (cursorPosition.column() < wordBegin || cursorPosition.column() > offset)) {
218 result.insert(textView.mid(wordBegin, offset - wordBegin));
219 }
220 }
221 wordBegin = offset + 1;
222 }
223 if (c.isSpace()) {
224 wordBegin = offset + 1;
225 }
226 offset += 1;
227 }
228 }
229
230 // ensure words that are ok spell check wise always end up in the completion, see bug 468705
231 const auto language = static_cast<KTextEditor::DocumentPrivate *>(document)->defaultDictionary();
232 const auto word = view->document()->text(range);
233 Sonnet::Speller speller;
234 QStringList spellerSuggestions;
235 speller.setLanguage(language);
236 if (speller.isValid()) {
237 if (speller.isCorrect(word)) {
238 result.insert(word);
239 } else {
240 spellerSuggestions = speller.suggest(word);
241 for (const auto &alternative : std::as_const(spellerSuggestions)) {
242 result.insert(alternative);
243 }
244 }
245 }
246
247 m_matches.clear();
248 m_matches.reserve(result.size());
249 for (auto v : std::as_const(result)) {
250 m_matches << v.toString();
251 }
252
253 return m_matches;
254}
255
256void KateWordCompletionModel::executeCompletionItem(KTextEditor::View *view, const KTextEditor::Range &word, const QModelIndex &index) const
257{
258 view->document()->replaceText(word, m_matches.at(index.row()));
259}
260
261KTextEditor::CodeCompletionModelControllerInterface::MatchReaction KateWordCompletionModel::matchingItem(const QModelIndex & /*matched*/)
262{
263 return HideListIfAutomaticInvocation;
264}
265
266bool KateWordCompletionModel::shouldHideItemsWithEqualNames() const
267{
268 // We don't want word-completion items if the same items
269 // are available through more sophisticated completion models
270 return true;
271}
272
273// END KateWordCompletionModel
274
275// BEGIN KateWordCompletionView
276struct KateWordCompletionViewPrivate {
277 KTextEditor::MovingRange *liRange; // range containing last inserted text
278 KTextEditor::Range dcRange; // current range to be completed by directional completion
279 KTextEditor::Cursor dcCursor; // directional completion search cursor
280 int directionalPos; // be able to insert "" at the correct time
281 bool isCompleting; // true when the directional completion is doing a completion
282};
283
284KateWordCompletionView::KateWordCompletionView(KTextEditor::View *view, KActionCollection *ac)
285 : QObject(view)
286 , m_view(view)
287 , m_dWCompletionModel(KTextEditor::EditorPrivate::self()->wordCompletionModel())
288 , d(new KateWordCompletionViewPrivate)
289{
290 d->isCompleting = false;
291 d->dcRange = KTextEditor::Range::invalid();
292
293 d->liRange =
294 static_cast<KTextEditor::DocumentPrivate *>(m_view->document())->newMovingRange(KTextEditor::Range::invalid(), KTextEditor::MovingRange::DoNotExpand);
295
297 a->setBackground(static_cast<KTextEditor::ViewPrivate *>(view)->rendererConfig()->selectionColor());
298 d->liRange->setAttribute(a);
299
300 QAction *action;
301
302 action = new QAction(i18n("Shell Completion"), this);
303 ac->addAction(QStringLiteral("doccomplete_sh"), action);
305 connect(action, &QAction::triggered, this, &KateWordCompletionView::shellComplete);
306
307 action = new QAction(i18n("Reuse Word Above"), this);
308 ac->addAction(QStringLiteral("doccomplete_bw"), action);
311 connect(action, &QAction::triggered, this, &KateWordCompletionView::completeBackwards);
312
313 action = new QAction(i18n("Reuse Word Below"), this);
314 ac->addAction(QStringLiteral("doccomplete_fw"), action);
317 connect(action, &QAction::triggered, this, &KateWordCompletionView::completeForwards);
318}
319
320KateWordCompletionView::~KateWordCompletionView()
321{
322 delete d;
323}
324
325void KateWordCompletionView::completeBackwards()
326{
327 complete(false);
328}
329
330void KateWordCompletionView::completeForwards()
331{
332 complete();
333}
334
335// Pop up the editors completion list if applicable
336void KateWordCompletionView::popupCompletionList()
337{
338 qCDebug(LOG_KTE) << "entered ...";
339 KTextEditor::Range r = range();
340
341 if (m_view->isCompletionActive()) {
342 return;
343 }
344
345 m_dWCompletionModel->saveMatches(m_view, r);
346
347 qCDebug(LOG_KTE) << "after save matches ...";
348
349 if (!m_dWCompletionModel->rowCount(QModelIndex())) {
350 return;
351 }
352
353 m_view->startCompletion(r, m_dWCompletionModel);
354}
355
356// Contributed by <brain@hdsnet.hu>
357void KateWordCompletionView::shellComplete()
358{
359 KTextEditor::Range r = range();
360
361 const QStringList matches = m_dWCompletionModel->allMatches(m_view, r);
362
363 if (matches.size() == 0) {
364 return;
365 }
366
367 QString partial = findLongestUnique(matches, r.columnWidth());
368
369 if (partial.isEmpty()) {
370 popupCompletionList();
371 }
372
373 else {
374 m_view->document()->insertText(r.end(), partial.mid(r.columnWidth()));
375 d->liRange->setView(m_view);
376 d->liRange->setRange(KTextEditor::Range(r.end(), partial.length() - r.columnWidth()));
377 connect(m_view, &KTextEditor::View::cursorPositionChanged, this, &KateWordCompletionView::slotCursorMoved);
378 }
379}
380
381// Do one completion, searching in the desired direction,
382// if possible
383void KateWordCompletionView::complete(bool fw)
384{
385 KTextEditor::Range r = range();
386
387 int inc = fw ? 1 : -1;
388 KTextEditor::Document *doc = m_view->document();
389
390 if (d->dcRange.isValid()) {
391 // qCDebug(LOG_KTE)<<"CONTINUE "<<d->dcRange;
392 // this is a repeated activation
393
394 // if we are back to where we started, reset.
395 if ((fw && d->directionalPos == -1) || (!fw && d->directionalPos == 1)) {
396 const int spansColumns = d->liRange->end().column() - d->liRange->start().column();
397 if (spansColumns > 0) {
398 doc->removeText(*d->liRange);
399 }
400
401 d->liRange->setRange(KTextEditor::Range::invalid());
402 d->dcCursor = r.end();
403 d->directionalPos = 0;
404
405 return;
406 }
407
408 if (fw) {
409 const int spansColumns = d->liRange->end().column() - d->liRange->start().column();
410 d->dcCursor.setColumn(d->dcCursor.column() + spansColumns);
411 }
412
413 d->directionalPos += inc;
414 } else { // new completion, reset all
415 // qCDebug(LOG_KTE)<<"RESET FOR NEW";
416 d->dcRange = r;
417 d->liRange->setRange(KTextEditor::Range::invalid());
418 d->dcCursor = r.start();
419 d->directionalPos = inc;
420
421 d->liRange->setView(m_view);
422
423 connect(m_view, &KTextEditor::View::cursorPositionChanged, this, &KateWordCompletionView::slotCursorMoved);
424 }
425
426 const QRegularExpression wordRegEx(QLatin1String("\\b") + doc->text(d->dcRange) + QLatin1String("(\\w+)"), QRegularExpression::UseUnicodePropertiesOption);
427 int pos(0);
428 QString ln = doc->line(d->dcCursor.line());
429
430 while (true) {
431 // qCDebug(LOG_KTE)<<"SEARCHING FOR "<<wordRegEx.pattern()<<" "<<ln<<" at "<<d->dcCursor;
433 pos = fw ? ln.indexOf(wordRegEx, d->dcCursor.column(), &match) : ln.lastIndexOf(wordRegEx, d->dcCursor.column(), &match);
434
435 if (match.hasMatch()) { // we matched a word
436 // qCDebug(LOG_KTE)<<"USABLE MATCH";
437 const QStringView m = match.capturedView(1);
438 if (m != doc->text(*d->liRange) && (d->dcCursor.line() != d->dcRange.start().line() || pos != d->dcRange.start().column())) {
439 // we got good a match! replace text and return.
440 d->isCompleting = true;
441 KTextEditor::Range replaceRange(d->liRange->toRange());
442 if (!replaceRange.isValid()) {
443 replaceRange.setRange(r.end(), r.end());
444 }
445 doc->replaceText(replaceRange, m.toString());
446 d->liRange->setRange(KTextEditor::Range(d->dcRange.end(), m.length()));
447
448 d->dcCursor.setColumn(pos); // for next try
449
450 d->isCompleting = false;
451 return;
452 }
453
454 // equal to last one, continue
455 else {
456 // qCDebug(LOG_KTE)<<"SKIPPING, EQUAL MATCH";
457 d->dcCursor.setColumn(pos); // for next try
458
459 if (fw) {
460 d->dcCursor.setColumn(pos + m.length());
461 }
462
463 else {
464 if (pos == 0) {
465 if (d->dcCursor.line() > 0) {
466 int l = d->dcCursor.line() + inc;
467 ln = doc->line(l);
468 d->dcCursor.setPosition(l, ln.length());
469 } else {
470 return;
471 }
472 }
473
474 else {
475 d->dcCursor.setColumn(d->dcCursor.column() - 1);
476 }
477 }
478 }
479 }
480
481 else { // no match
482 // qCDebug(LOG_KTE)<<"NO MATCH";
483 if ((!fw && d->dcCursor.line() == 0) || (fw && d->dcCursor.line() >= doc->lines())) {
484 return;
485 }
486
487 int l = d->dcCursor.line() + inc;
488 ln = doc->line(l);
489 d->dcCursor.setPosition(l, fw ? 0 : ln.length());
490 }
491 } // while true
492}
493
494void KateWordCompletionView::slotCursorMoved()
495{
496 if (d->isCompleting) {
497 return;
498 }
499
500 d->dcRange = KTextEditor::Range::invalid();
501
502 disconnect(m_view, &KTextEditor::View::cursorPositionChanged, this, &KateWordCompletionView::slotCursorMoved);
503
504 d->liRange->setView(nullptr);
505 d->liRange->setRange(KTextEditor::Range::invalid());
506}
507
508// Contributed by <brain@hdsnet.hu> FIXME
509QString KateWordCompletionView::findLongestUnique(const QStringList &matches, int lead)
510{
511 QString partial = matches.first();
512
513 for (const QString &current : matches) {
514 if (!current.startsWith(partial)) {
515 while (partial.length() > lead) {
516 partial.remove(partial.length() - 1, 1);
517 if (current.startsWith(partial)) {
518 break;
519 }
520 }
521
522 if (partial.length() == lead) {
523 return QString();
524 }
525 }
526 }
527
528 return partial;
529}
530
531// Return the string to complete (the letters behind the cursor)
532QString KateWordCompletionView::word() const
533{
534 return m_view->document()->text(range());
535}
536
537// Return the range containing the word behind the cursor
538KTextEditor::Range KateWordCompletionView::range() const
539{
540 return m_dWCompletionModel->completionRange(m_view, m_view->cursorPosition());
541}
542// END
543
544#include "moc_katewordcompletion.cpp"
QAction * addAction(const QString &name, const QObject *receiver=nullptr, const char *member=nullptr)
static void setDefaultShortcut(QAction *action, const QKeySequence &shortcut)
A class which provides customized text decorations.
Definition attribute.h:51
QExplicitlySharedDataPointer< Attribute > Ptr
Shared data pointer for Attribute.
Definition attribute.h:56
virtual Range completionRange(View *view, const Cursor &position)
This function returns the completion range that will be used for the current completion.
@ Icon
Icon representing the type of completion.
@ InheritanceDepth
Returns the inheritance depth of the completion.
@ GroupRole
Using this Role, it is possible to greatly optimize the time needed to process very long completion-l...
@ UnimportantItemRole
Return a nonzero value here to enforce sorting the item at the end of the list.
The Cursor represents a position in a Document.
Definition cursor.h:75
constexpr int column() const noexcept
Retrieve the column on which this cursor is situated.
Definition cursor.h:192
constexpr int line() const noexcept
Retrieve the line on which this cursor is situated.
Definition cursor.h:174
A KParts derived class representing a text document.
Definition document.h:284
virtual QString line(int line) const =0
Get a single text line.
virtual bool replaceText(Range range, const QString &text, bool block=false)
Replace text from range with specified text.
Definition document.cpp:85
virtual QString text() const =0
Get the document content.
virtual bool removeText(Range range, bool block=false)=0
Remove the text specified in range.
virtual int lines() const =0
Get the count of lines of the document.
virtual bool insertText(KTextEditor::Cursor position, const QString &text, bool block=false)=0
Insert text at position.
A range that is bound to a specific Document, and maintains its position.
@ DoNotExpand
Don't expand to encapsulate new characters in either direction. This is the default.
An object representing a section of text, from one Cursor to another.
constexpr Cursor end() const noexcept
Get the end position of this range.
constexpr Cursor start() const noexcept
Get the start position of this range.
constexpr int columnWidth() const noexcept
Returns the number of columns separating the start() and end() positions.
static constexpr Range invalid() noexcept
Returns an invalid range.
A text widget with KXMLGUIClient that represents a Document.
Definition view.h:244
virtual Document * document() const =0
Get the view's document, that means the view is a view of the returned document.
virtual Cursor cursorPosition() const =0
Get the view's current cursor position.
void cursorPositionChanged(KTextEditor::View *view, KTextEditor::Cursor newPosition)
This signal is emitted whenever the view's cursor position changed.
virtual bool isCompletionActive() const =0
Completion.
virtual void startCompletion(Range word, CodeCompletionModel *model)=0
Invoke code completion over a given range, with a specific model.
QStringList suggest(const QString &word) const
bool isCorrect(const QString &word) const
bool isValid() const
void setLanguage(const QString &lang)
Q_SCRIPTABLE Q_NOREPLY void start()
QString i18n(const char *text, const TYPE &arg...)
KCOREADDONS_EXPORT Result match(QStringView pattern, QStringView str)
const QList< QKeySequence > & end()
The KTextEditor namespace contains all the public API that is required to use the KTextEditor compone...
QModelIndex createIndex(int row, int column, const void *ptr) const const
void setShortcutContext(Qt::ShortcutContext context)
void triggered(bool checked)
bool isLetter(char32_t ucs4)
bool isLetterOrNumber(char32_t ucs4)
bool isNumber(char32_t ucs4)
bool isSpace(char32_t ucs4)
QIcon fromTheme(const QString &name)
const_reference at(qsizetype i) const const
void clear()
qsizetype count() const const
T & first()
bool isEmpty() const const
void reserve(qsizetype size)
qsizetype size() const const
int column() const const
quintptr internalId() const const
bool isValid() const const
QModelIndex parent() const const
int row() const const
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
bool disconnect(const QMetaObject::Connection &connection)
QObject * parent() const const
iterator insert(const T &value)
qsizetype size() const const
const QChar at(qsizetype position) const const
QChar * data()
qsizetype indexOf(QChar ch, qsizetype from, Qt::CaseSensitivity cs) const const
bool isEmpty() const const
bool isNull() const const
qsizetype lastIndexOf(QChar ch, Qt::CaseSensitivity cs) const const
QString left(qsizetype n) const const
qsizetype length() const const
QString mid(qsizetype position, qsizetype n) const const
QString & remove(QChar ch, Qt::CaseSensitivity cs)
qsizetype size() const const
void sort(Qt::CaseSensitivity cs)
QStringView mid(qsizetype start, qsizetype length) const const
qsizetype length() const const
QString toString() const const
DisplayRole
WidgetWithChildrenShortcut
QFuture< ArgsType< Signal > > connect(Sender *sender, Signal signal)
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Tue Mar 26 2024 11:15:43 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.