1 /*
2 SPDX-FileCopyrightText: 2021 Ilia Kats <ilia-kats@gmx.net>
3
4 SPDX-License-Identifier: LGPL-2.0-or-later
5 */
6
7 #include "completionmodel.h"
8 #include "completiontable.h"
9
10 #include <algorithm>
11 #include <string>
12
13 #include <QIcon>
14 #include <QRegularExpression>
15
16 #include <KTextEditor/Document>
17 #include <KTextEditor/View>
18
startsWith(const Completion & comp,const std::u16string & prefix)19 bool startsWith(const Completion &comp, const std::u16string &prefix)
20 {
21 if (prefix.size() <= comp.completion_strlen)
22 return std::char_traits<char16_t>::compare(prefix.data(), comp.completion, prefix.size()) == 0;
23 return false;
24 }
25
LatexCompletionModel(QObject * parent)26 LatexCompletionModel::LatexCompletionModel(QObject *parent)
27 : KTextEditor::CodeCompletionModel(parent)
28 {
29 }
30
completionInvoked(KTextEditor::View * view,const KTextEditor::Range & range,KTextEditor::CodeCompletionModel::InvocationType invocationType)31 void LatexCompletionModel::completionInvoked(KTextEditor::View *view,
32 const KTextEditor::Range &range,
33 KTextEditor::CodeCompletionModel::InvocationType invocationType)
34 {
35 Q_UNUSED(invocationType);
36 beginResetModel();
37 m_matches.first = m_matches.second = -1;
38 auto word = view->document()->text(range).toStdU16String();
39 const Completion *beginit = (Completion *)&completiontable;
40 const Completion *endit = beginit + n_completions;
41 if (!word.empty() && word[0] == QLatin1Char('\\')) {
42 auto prefixrangestart = std::lower_bound(beginit, endit, word, [](const Completion &a, const std::u16string &b) -> bool {
43 return startsWith(a, b) ? false : a.completion < b;
44 });
45 auto prefixrangeend = std::upper_bound(beginit, endit, word, [](const std::u16string &a, const Completion &b) -> bool {
46 return startsWith(b, a) ? false : a < b.completion;
47 });
48 if (prefixrangestart != endit) {
49 m_matches.first = prefixrangestart - beginit;
50 m_matches.second = prefixrangeend - beginit;
51 }
52 }
53 setRowCount(m_matches.second - m_matches.first);
54 endResetModel();
55 }
56
shouldStartCompletion(KTextEditor::View * view,const QString & insertedText,bool userInsertion,const KTextEditor::Cursor & position)57 bool LatexCompletionModel::shouldStartCompletion(KTextEditor::View *view, const QString &insertedText, bool userInsertion, const KTextEditor::Cursor &position)
58 {
59 Q_UNUSED(view);
60 Q_UNUSED(position);
61 return userInsertion && latexexpr.match(insertedText).hasMatch();
62 }
63
shouldAbortCompletion(KTextEditor::View * view,const KTextEditor::Range & range,const QString & currentCompletion)64 bool LatexCompletionModel::shouldAbortCompletion(KTextEditor::View *view, const KTextEditor::Range &range, const QString ¤tCompletion)
65 {
66 if (view->cursorPosition() < range.start() || view->cursorPosition() > range.end())
67 return true;
68 return !latexexpr.match(currentCompletion).hasMatch();
69 }
70
completionRange(KTextEditor::View * view,const KTextEditor::Cursor & position)71 KTextEditor::Range LatexCompletionModel::completionRange(KTextEditor::View *view, const KTextEditor::Cursor &position)
72 {
73 auto text = view->document()->line(position.line());
74 KTextEditor::Cursor start = position;
75 int pos = text.left(position.column()).lastIndexOf(latexexpr);
76 if (pos >= 0)
77 start.setColumn(pos);
78 return KTextEditor::Range(start, position);
79 }
80
executeCompletionItem(KTextEditor::View * view,const KTextEditor::Range & word,const QModelIndex & index) const81 void LatexCompletionModel::executeCompletionItem(KTextEditor::View *view, const KTextEditor::Range &word, const QModelIndex &index) const
82 {
83 view->document()->replaceText(word, data(index.sibling(index.row(), Postfix), Qt::DisplayRole).toString());
84 }
85
data(const QModelIndex & index,int role) const86 QVariant LatexCompletionModel::data(const QModelIndex &index, int role) const
87 {
88 if (role == UnimportantItemRole)
89 return false;
90 else if (role == InheritanceDepth)
91 return 1;
92
93 if (index.isValid() && index.row() < m_matches.second - m_matches.first) {
94 const Completion &completion = completiontable[m_matches.first + index.row()];
95 if (role == IsExpandable)
96 return true; // if it's not expandable, the description will often be cut off
97 // because apprarently the ItemSelected role is not taken into account
98 // when determining the completion widget width. So expanding is
99 // the only way to make sure that the complete description is available.
100 else if (role == ItemSelected || role == ExpandingWidget)
101 return QStringLiteral("<table><tr><td>%1</td><td>%2</td></tr></table>")
102 .arg(QString::fromUtf16(completion.codepoint), QString::fromUtf16(completion.name));
103 else if (role == Qt::DisplayRole) {
104 if (index.column() == Name)
105 return QString::fromUtf16(completion.completion);
106 else if (index.column() == Postfix)
107 return QString::fromUtf16(completion.chars);
108 } else if (index.column() == Icon && role == Qt::DecorationRole) {
109 static const QIcon icon(QIcon::fromTheme(QStringLiteral("texcompiler")));
110 return icon;
111 }
112 }
113 return QVariant();
114 }
115