1 /*
2     SPDX-FileCopyrightText: 2015 Michal Humpula <michal.humpula@hudrydum.cz>
3 
4     SPDX-License-Identifier: LGPL-2.0-or-later
5 */
6 
7 #include "wordcounter.h"
8 #include "katedocument.h"
9 #include "kateview.h"
10 
WordCounter(KTextEditor::ViewPrivate * view)11 WordCounter::WordCounter(KTextEditor::ViewPrivate *view)
12     : QObject(view)
13     , m_wordsInDocument(0)
14     , m_wordsInSelection(0)
15     , m_charsInDocument(0)
16     , m_charsInSelection(0)
17     , m_startRecalculationFrom(0)
18     , m_document(view->document())
19 {
20     connect(view->doc(), &KTextEditor::DocumentPrivate::textInsertedRange, this, &WordCounter::textInserted);
21     connect(view->doc(), &KTextEditor::DocumentPrivate::textRemoved, this, &WordCounter::textRemoved);
22     connect(view->doc(), &KTextEditor::DocumentPrivate::loaded, this, &WordCounter::recalculate);
23     connect(view, &KTextEditor::View::selectionChanged, this, &WordCounter::selectionChanged);
24 
25     m_timer.setInterval(500);
26     m_timer.setSingleShot(true);
27     connect(&m_timer, &QTimer::timeout, this, &WordCounter::recalculateLines);
28 
29     recalculate(m_document);
30 }
31 
textInserted(KTextEditor::Document *,const KTextEditor::Range & range)32 void WordCounter::textInserted(KTextEditor::Document *, const KTextEditor::Range &range)
33 {
34     auto startLine = m_countByLine.begin() + range.start().line();
35     auto endLine = m_countByLine.begin() + range.end().line();
36     size_t newLines = std::distance(startLine, endLine);
37 
38     if (m_countByLine.empty()) { // was empty document before insert
39         newLines++;
40     }
41 
42     if (newLines > 0) {
43         m_countByLine.insert(startLine, newLines, -1);
44     }
45 
46     m_countByLine[range.end().line()] = -1;
47     m_timer.start();
48 }
49 
textRemoved(KTextEditor::Document *,const KTextEditor::Range & range,const QString &)50 void WordCounter::textRemoved(KTextEditor::Document *, const KTextEditor::Range &range, const QString &)
51 {
52     const auto startLine = m_countByLine.begin() + range.start().line();
53     const auto endLine = m_countByLine.begin() + range.end().line();
54     const int removedLines = endLine - startLine;
55 
56     if (removedLines > 0) {
57         m_countByLine.erase(startLine, endLine);
58     }
59 
60     if (!m_countByLine.empty()) {
61         m_countByLine[range.start().line()] = -1;
62         m_timer.start();
63     } else {
64         Q_EMIT changed(0, 0, 0, 0);
65     }
66 }
67 
recalculate(KTextEditor::Document *)68 void WordCounter::recalculate(KTextEditor::Document *)
69 {
70     m_countByLine = std::vector<int>(m_document->lines(), -1);
71     m_timer.start();
72 }
73 
countWords(const QString & text)74 static int countWords(const QString &text)
75 {
76     int count = 0;
77     bool inWord = false;
78 
79     for (const QChar c : text) {
80         if (c.isLetterOrNumber()) {
81             if (!inWord) {
82                 inWord = true;
83             }
84         } else {
85             if (inWord) {
86                 inWord = false;
87                 count++;
88             }
89         }
90     }
91 
92     return inWord ? count + 1 : count;
93 }
94 
selectionChanged(KTextEditor::View * view)95 void WordCounter::selectionChanged(KTextEditor::View *view)
96 {
97     if (view->selectionRange().isEmpty()) {
98         m_wordsInSelection = m_charsInSelection = 0;
99         Q_EMIT changed(m_wordsInDocument, 0, m_charsInDocument, 0);
100         return;
101     }
102 
103     const int firstLine = view->selectionRange().start().line();
104     const int lastLine = view->selectionRange().end().line();
105 
106     if (firstLine == lastLine || view->blockSelection()) {
107         const QString text = view->selectionText();
108         m_wordsInSelection = countWords(text);
109         m_charsInSelection = text.size();
110     } else {
111         m_wordsInSelection = m_charsInSelection = 0;
112 
113         const KTextEditor::Range firstLineRange(view->selectionRange().start(), firstLine, view->document()->lineLength(firstLine));
114         const QString firstLineText = view->document()->text(firstLineRange);
115         m_wordsInSelection += countWords(firstLineText);
116         m_charsInSelection += firstLineText.size();
117 
118         // whole lines
119         for (int i = firstLine + 1; i < lastLine; i++) {
120             m_wordsInSelection += m_countByLine[i];
121             m_charsInSelection += m_document->lineLength(i);
122         }
123 
124         const KTextEditor::Range lastLineRange(KTextEditor::Cursor(lastLine, 0), view->selectionRange().end());
125         const QString lastLineText = view->document()->text(lastLineRange);
126         m_wordsInSelection += countWords(lastLineText);
127         m_charsInSelection += lastLineText.size();
128     }
129 
130     Q_EMIT changed(m_wordsInDocument, m_wordsInSelection, m_charsInDocument, m_charsInSelection);
131 }
132 
recalculateLines()133 void WordCounter::recalculateLines()
134 {
135     if ((size_t)m_startRecalculationFrom >= m_countByLine.size()) {
136         m_startRecalculationFrom = 0;
137     }
138 
139     int wordsCount = 0;
140     int charsCount = 0;
141     int calculated = 0;
142     size_t i = m_startRecalculationFrom;
143     constexpr int MaximumLinesToRecalculate = 100;
144 
145     // stay in bounds, vector might be empty, even 0 is too large then
146     while (i < m_countByLine.size()) {
147         if (m_countByLine[i] == -1) {
148             m_countByLine[i] = countWords(m_document->line(i));
149             if (++calculated > MaximumLinesToRecalculate) {
150                 m_startRecalculationFrom = i;
151                 m_timer.start();
152                 return;
153             }
154         }
155 
156         wordsCount += m_countByLine[i];
157         charsCount += m_document->lineLength(i);
158 
159         if (++i == m_countByLine.size()) { // array cycle
160             i = 0;
161         }
162 
163         if (i == (size_t)m_startRecalculationFrom) {
164             break;
165         }
166     }
167 
168     m_wordsInDocument = wordsCount;
169     m_charsInDocument = charsCount;
170     Q_EMIT changed(m_wordsInDocument, m_wordsInSelection, m_charsInDocument, m_charsInSelection);
171 }
172