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 &currentCompletion)
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