[kde-doc-english] [ktexteditor] src: add word count statistics in statusbar

Michal Humpula michal.humpula at hudrydum.cz
Fri Feb 20 15:52:27 UTC 2015


Git commit 97a1fc436f9810c6ba1c35ed42080ef1829871aa by Michal Humpula.
Committed on 14/02/2015 at 19:01.
Pushed by michalhumpula into branch 'master'.

add word count statistics in statusbar

This option is disabled by default, because it adds some nontrivial CPU and
memory overhead, though the implementation tries to keep it as minimal
as possible.

Whenewer the change in document is triggered, the timer is fired to wait
for 500ms of inactivity. The trigger is reseted every time the change
happens so as long as she keeps changing the document every 500ms
or so, the counting wont stall the editor and her experience.

Longer reculculations are done in batches of size 100 with the help of
500ms trigger. Which means 10000 lines long document will take 20s to
compute. Subsequent counting of changes will be much faster because of
the internal computation cache.

Interface changes includes adding signal KTextEditor::DocumentPrivate::loaded
to monitor document reload.

GUI: menu: View -> Show Word Count
REVIEW: 122571
BUG: 65740
FIXED-IN: 5.8.0

M  +2    -1    src/CMakeLists.txt
M  +2    -1    src/data/katepartui.rc
M  +9    -8    src/document/katedocument.cpp
M  +6    -3    src/document/katedocument.h
M  +22   -0    src/utils/kateconfig.cpp
M  +4    -0    src/utils/kateconfig.h
M  +44   -0    src/view/katestatusbar.cpp
M  +14   -2    src/view/katestatusbar.h
M  +11   -0    src/view/kateview.cpp
M  +2    -0    src/view/kateview.h
A  +189  -0    src/view/wordcounter.cpp     [License: LGPL (v2)]
A  +63   -0    src/view/wordcounter.h     [License: LGPL (v2)]

http://commits.kde.org/ktexteditor/97a1fc436f9810c6ba1c35ed42080ef1829871aa

diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 16ec336..70c4d89 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -143,6 +143,7 @@ view/katefadeeffect.cpp
 view/kateanimation.cpp
 view/katetextanimation.cpp
 view/katestatusbar.cpp
+view/wordcounter.cpp
 
 # spell checking
 spellcheck/prefixstore.h
@@ -207,7 +208,7 @@ inputmode/katenormalinputmode.cpp
 inputmode/katenormalinputmodefactory.cpp
 )
 
-ki18n_wrap_ui(ktexteditor_LIB_SRCS 
+ki18n_wrap_ui(ktexteditor_LIB_SRCS
 dialogs/textareaappearanceconfigwidget.ui
 dialogs/bordersappearanceconfigwidget.ui
 dialogs/commandmenuconfigwidget.ui
diff --git a/src/data/katepartui.rc b/src/data/katepartui.rc
index 71d576f..659c3f5 100644
--- a/src/data/katepartui.rc
+++ b/src/data/katepartui.rc
@@ -1,5 +1,5 @@
 <!DOCTYPE kpartgui SYSTEM "kpartgui.dtd">
-<kpartgui name="KatePartView" version="75" translationDomain="ktexteditor5">
+<kpartgui name="KatePartView" version="76" translationDomain="ktexteditor5">
 <MenuBar>
   <Menu name="file" noMerge="1"><text>&File</text>
     <Action name="file_save" group="save_merge" />
@@ -93,6 +93,7 @@
     </Menu>
     <Separator group="view_operations" />
     <Action name="view_non_printable_spaces" group="view_operations" />
+    <Action name="view_word_count" group="view_operations" />
   </Menu>
 
   <Action name="bookmarks" />
diff --git a/src/document/katedocument.cpp b/src/document/katedocument.cpp
index 917de3c..4f66a47 100644
--- a/src/document/katedocument.cpp
+++ b/src/document/katedocument.cpp
@@ -120,7 +120,7 @@ static QUrl normalizeUrl (const QUrl &url)
      */
     if (url.isEmpty() || !url.isLocalFile())
         return url;
-    
+
     /**
      * don't normalize if not existing!
      * canonicalFilePath won't work!
@@ -128,7 +128,7 @@ static QUrl normalizeUrl (const QUrl &url)
     const QString normalizedUrl(QFileInfo(url.toLocalFile()).canonicalFilePath());
     if (normalizedUrl.isEmpty())
         return url;
-    
+
     /**
      * else: use canonicalFilePath to normalize
      */
@@ -179,7 +179,7 @@ KTextEditor::DocumentPrivate::DocumentPrivate(bool bSingleViewMode,
      * no plugins from kparts here
      */
     setPluginLoadingMode (DoNotLoadPlugins);
-    
+
     /**
      * pass on our component data, do this after plugin loading is off
      */
@@ -218,7 +218,7 @@ KTextEditor::DocumentPrivate::DocumentPrivate(bool bSingleViewMode,
 
     connect(KTextEditor::EditorPrivate::self()->dirWatch(), SIGNAL(deleted(QString)),
             this, SLOT(slotModOnHdDeleted(QString)));
-    
+
     /**
      * singleshot timer to handle updates of mod on hd state delayed
      */
@@ -2252,6 +2252,7 @@ bool KTextEditor::DocumentPrivate::openFile()
 
     // Inform that the text has changed (required as we're not inside the usual editStart/End stuff)
     emit textChanged(this);
+    emit loaded(this);
 
     //
     // to houston, we are not modified
@@ -4563,7 +4564,7 @@ void KTextEditor::DocumentPrivate::slotModOnHdDirty(const QString &path)
     if ((path == m_dirWatchFile) && (!m_modOnHd || m_modOnHdReason != OnDiskModified)) {
         m_modOnHd = true;
         m_modOnHdReason = OnDiskModified;
-        
+
         if (!m_modOnHdTimer.isActive()) {
             m_modOnHdTimer.start();
         }
@@ -4646,7 +4647,7 @@ void KTextEditor::DocumentPrivate::slotDelayedHandleModOnHd()
         }
 #endif
     }
-    
+
     /**
      * reenable dialog if not running atm
      */
@@ -4680,7 +4681,7 @@ bool KTextEditor::DocumentPrivate::createDigest()
             while (!f.atEnd()) {
                 crypto.addData(f.read(256 * 1024));
             }
-            
+
             digest = crypto.result();
         }
     }
@@ -5702,7 +5703,7 @@ int KTextEditor::DocumentPrivate::defStyleNum(int line, int column)
     } else {
         return -1;
     }
-    
+
     return highlight()->defaultStyleForAttribute(attribute);
 }
 
diff --git a/src/document/katedocument.h b/src/document/katedocument.h
index 94dff64..ebb6786 100644
--- a/src/document/katedocument.h
+++ b/src/document/katedocument.h
@@ -139,7 +139,7 @@ private:
     //
 public:
     KTextEditor::View *createView(QWidget *parent, KTextEditor::MainWindow *mainWindow = Q_NULLPTR) Q_DECL_OVERRIDE;
-    
+
     QList<KTextEditor::View *> views() const Q_DECL_OVERRIDE
     {
         return m_views.keys();
@@ -498,7 +498,7 @@ public:
 public:
     /**
      * Read session settings from the given \p config.
-     * 
+     *
      * Known flags:
      *  "SkipUrl" => don't save/restore the file
      *  "SkipMode" => don't save/restore the mode
@@ -1380,7 +1380,7 @@ public:
 
 public Q_SLOTS:
     void openWithLineLengthLimitOverride();
-    
+
 private:
     /**
      * timer for delayed handling of mod on hd
@@ -1395,6 +1395,9 @@ private:
 
 public:
     void setActiveTemplateHandler(KateTemplateHandler* handler);
+
+Q_SIGNALS:
+    void loaded(KTextEditor::DocumentPrivate *document);
 };
 
 #endif
diff --git a/src/utils/kateconfig.cpp b/src/utils/kateconfig.cpp
index 0426de6..ed6505b 100644
--- a/src/utils/kateconfig.cpp
+++ b/src/utils/kateconfig.cpp
@@ -1199,6 +1199,7 @@ void KateDocumentConfig::setLineLengthLimit(int lineLengthLimit)
 //BEGIN KateViewConfig
 KateViewConfig::KateViewConfig()
     :
+    m_showWordCount(false),
     m_dynWordWrapSet(false),
     m_dynWordWrapIndicatorsSet(false),
     m_dynWordWrapAlignIndentSet(false),
@@ -1241,6 +1242,7 @@ KateViewConfig::KateViewConfig(KTextEditor::ViewPrivate *view)
     :
     m_searchFlags(PowerModePlainText),
     m_maxHistorySize(100),
+    m_showWordCount(false),
     m_dynWordWrapSet(false),
     m_dynWordWrapIndicatorsSet(false),
     m_dynWordWrapAlignIndentSet(false),
@@ -1310,6 +1312,7 @@ const char KEY_WORD_COMPLETION_REMOVE_TAIL[] = "Word Completion Remove Tail";
 const char KEY_SMART_COPY_CUT[] = "Smart Copy Cut";
 const char KEY_SCROLL_PAST_END[] = "Scroll Past End";
 const char KEY_FOLD_FIRST_LINE[] = "Fold First Line";
+const char KEY_SHOW_WORD_COUNT[] = "Show Word Count";
 }
 
 void KateViewConfig::readConfig(const KConfigGroup &config)
@@ -1365,6 +1368,7 @@ void KateViewConfig::readConfig(const KConfigGroup &config)
     setSmartCopyCut(config.readEntry(KEY_SMART_COPY_CUT, false));
     setScrollPastEnd(config.readEntry(KEY_SCROLL_PAST_END, false));
     setFoldFirstLine(config.readEntry(KEY_FOLD_FIRST_LINE, false));
+    setShowWordCount(config.readEntry(KEY_SHOW_WORD_COUNT, false));
 
     configEnd();
 }
@@ -1420,6 +1424,8 @@ void KateViewConfig::writeConfig(KConfigGroup &config)
     config.writeEntry(KEY_INPUT_MODE, static_cast<int>(inputMode()));
     config.writeEntry(KEY_VI_INPUT_MODE_STEAL_KEYS, viInputModeStealKeys());
     config.writeEntry(KEY_VI_RELATIVE_LINE_NUMBERS, viRelativeLineNumbers());
+
+    config.writeEntry(KEY_SHOW_WORD_COUNT, showWordCount());
 }
 
 void KateViewConfig::updateConfig()
@@ -2093,6 +2099,22 @@ void KateViewConfig::setFoldFirstLine(bool on)
     configEnd();
 }
 
+bool KateViewConfig::showWordCount()
+{
+    return m_showWordCount;
+}
+
+void KateViewConfig::setShowWordCount(bool on)
+{
+    if (m_showWordCount == on) {
+        return;
+    }
+
+    configStart();
+    m_showWordCount = on;
+    configEnd();
+}
+
 //END
 
 //BEGIN KateRendererConfig
diff --git a/src/utils/kateconfig.h b/src/utils/kateconfig.h
index 6edd61e..3b382b9 100644
--- a/src/utils/kateconfig.h
+++ b/src/utils/kateconfig.h
@@ -555,6 +555,9 @@ public:
     bool foldFirstLine() const;
     void setFoldFirstLine(bool on);
 
+    bool showWordCount();
+    void setShowWordCount(bool on);
+
 private:
     bool m_dynWordWrap;
     int m_dynWordWrapIndicators;
@@ -585,6 +588,7 @@ private:
     bool m_smartCopyCut;
     bool m_scrollPastEnd;
     bool m_foldFirstLine;
+    bool m_showWordCount;
 
     bool m_dynWordWrapSet : 1;
     bool m_dynWordWrapIndicatorsSet : 1;
diff --git a/src/view/katestatusbar.cpp b/src/view/katestatusbar.cpp
index 2c256da..b7e9c44 100644
--- a/src/view/katestatusbar.cpp
+++ b/src/view/katestatusbar.cpp
@@ -26,6 +26,7 @@
 #include "katedocument.h"
 #include "kateconfig.h"
 #include "kateabstractinputmode.h"
+#include "wordcounter.h"
 
 #include <KLocalizedString>
 #include <KIconLoader>
@@ -57,6 +58,7 @@ KateStatusBar::KateStatusBar(KTextEditor::ViewPrivate *view)
     , m_insertModeLabel(Q_NULLPTR)
     , m_modifiedStatus (-1)
     , m_selectionMode (-1)
+    , m_wordCounter(Q_NULLPTR)
 {
     KAcceleratorManager::setNoAccel(this);
     setFocusProxy(m_view);
@@ -84,6 +86,16 @@ KateStatusBar::KateStatusBar(KTextEditor::ViewPrivate *view)
     m_lineColLabel->setWhatsThis(i18n("Current cursor position. Doubleclick to go to specific line."));
 
     /**
+     * show word count
+     */
+    m_wordCountLabel = new QLabel(this);
+    topLayout->addWidget(m_wordCountLabel, 0);
+    m_wordCountLabel->setAlignment(Qt::AlignVCenter | Qt::AlignLeft);
+    m_wordCountLabel->setFocusProxy(m_view);
+    m_wordCountLabel->setWhatsThis(i18n("Words and Chars count in document/selection."));
+    m_wordCountLabel->hide();
+
+    /**
      * show the current mode, like INSERT, OVERWRITE, VI + modifiers like [BLOCK]
      */
     m_insertModeLabel = new QLabel( this );
@@ -183,11 +195,13 @@ KateStatusBar::KateStatusBar(KTextEditor::ViewPrivate *view)
     connect(m_view->document(), SIGNAL(modifiedOnDisk(KTextEditor::Document*,bool,KTextEditor::ModificationInterface::ModifiedOnDiskReason)), this, SLOT(modifiedChanged()) );
     connect(m_view->document(), SIGNAL(configChanged()), this, SLOT(documentConfigChanged()));
     connect(m_view->document(), SIGNAL(modeChanged(KTextEditor::Document*)), this, SLOT(modeChanged()));
+    connect(m_view, &KTextEditor::ViewPrivate::configChanged, this, &KateStatusBar::configChanged);
 
     connect(m_tabGroup,SIGNAL(triggered(QAction*)),this,SLOT(slotTabGroup(QAction*)));
     connect(m_indentGroup,SIGNAL(triggered(QAction*)),this,SLOT(slotIndentGroup(QAction*)));
     connect(radioGroup,SIGNAL(triggered(QAction*)),this,SLOT(slotIndentTabMode(QAction*)));
     updateStatus ();
+    wordCountChanged(0, 0, 0, 0);
 }
 
 bool KateStatusBar::eventFilter(QObject *obj, QEvent *event)
@@ -405,3 +419,33 @@ void KateStatusBar::slotIndentTabMode(QAction* a) {
         m_tabGroup->setEnabled(false);
     }
 }
+
+void KateStatusBar::toggleWordCount(bool on)
+{
+    if ((m_wordCounter != Q_NULLPTR) == on) {
+        return;
+    }
+
+    if (on) {
+        m_wordCounter = new WordCounter(m_view);
+        connect(m_wordCounter, &WordCounter::changed, this, &KateStatusBar::wordCountChanged);
+    } else {
+        delete m_wordCounter;
+        m_wordCounter = Q_NULLPTR;
+        wordCountChanged(0, 0, 0, 0);
+    }
+
+    m_wordCountLabel->setVisible(on);
+}
+
+void KateStatusBar::wordCountChanged(int wordsInDocument, int wordsInSelection, int charsInDocument, int charsInSelection)
+{
+    m_wordCountLabel->setText(i18n("Words %1/%2, Chars %3/%4",
+                                   wordsInDocument, wordsInSelection,
+                                   charsInDocument, charsInSelection));
+}
+
+void KateStatusBar::configChanged()
+{
+    toggleWordCount(m_view->config()->showWordCount());
+}
diff --git a/src/view/katestatusbar.h b/src/view/katestatusbar.h
index 648bc7e..1c399bb 100644
--- a/src/view/katestatusbar.h
+++ b/src/view/katestatusbar.h
@@ -31,6 +31,8 @@
 #include <QPushButton>
 #include <QToolButton>
 
+class WordCounter;
+
 class KateStatusBarOpenUpMenu: public QMenu
 {
         Q_OBJECT
@@ -62,12 +64,19 @@ public Q_SLOTS:
 
     void modeChanged ();
 
+    void wordCountChanged(int, int, int, int);
+
+    void toggleWordCount(bool on);
+
+    void configChanged();
+
 protected:
     bool eventFilter(QObject *obj, QEvent *event) Q_DECL_OVERRIDE;
-    
+
 private:
     KTextEditor::ViewPrivate *const m_view;
     QLabel* m_lineColLabel;
+    QLabel* m_wordCountLabel;
     QToolButton* m_modifiedLabel;
     QLabel* m_insertModeLabel;
     QPushButton* m_mode;
@@ -85,9 +94,12 @@ private:
     QAction *m_mixedAction;
     QAction *m_hardAction;
     QAction *m_softAction;
+    WordCounter *m_wordCounter;
+
+private:
     void addNumberAction(QActionGroup *group, QMenu *menu, int data);
     void updateGroup(QActionGroup *group, int w);
-    
+
 public Q_SLOTS:
     void slotTabGroup(QAction*);
     void slotIndentGroup(QAction*);
diff --git a/src/view/kateview.cpp b/src/view/kateview.cpp
index a1a3335..5bdd99a 100644
--- a/src/view/kateview.cpp
+++ b/src/view/kateview.cpp
@@ -740,6 +740,12 @@ void KTextEditor::ViewPrivate::setupActions()
     a->setWhatsThis(i18n("Show/hide bounding box around non-printable spaces"));
     connect(a, SIGNAL(triggered(bool)), SLOT(toggleNPSpaces()));
 
+    a = m_toggleWordCount = new KToggleAction(i18n("Show Word Count"), this);
+    a->setChecked(false);
+    ac->addAction(QLatin1String("view_word_count"), a);
+    a->setWhatsThis(i18n("Show/hide word count in status bar"));
+    connect(a, &QAction::triggered, this, &ViewPrivate::toggleWordCount);
+
     a = m_switchCmdLine = ac->addAction(QLatin1String("switch_to_cmd_line"));
     a->setText(i18n("Switch to Command Line"));
     ac->setDefaultShortcut(a, QKeySequence(Qt::Key_F7));
@@ -1648,6 +1654,11 @@ void KTextEditor::ViewPrivate::toggleNPSpaces()
     m_viewInternal->update(); // force redraw
 }
 
+void KTextEditor::ViewPrivate::toggleWordCount(bool on)
+{
+    config()->setShowWordCount(on);
+}
+
 void KTextEditor::ViewPrivate::setFoldingMarkersOn(bool enable)
 {
     config()->setFoldingBar(enable);
diff --git a/src/view/kateview.h b/src/view/kateview.h
index 05c9aab..0a283a9 100644
--- a/src/view/kateview.h
+++ b/src/view/kateview.h
@@ -571,6 +571,7 @@ public Q_SLOTS:
     void reloadFile();
     void toggleWWMarker();
     void toggleNPSpaces();
+    void toggleWordCount(bool on);
     void toggleWriteLock();
     void switchToCmdLine();
     void slotReadWriteChanged();
@@ -630,6 +631,7 @@ private:
     KSelectAction         *m_setDynWrapIndicators;
     KToggleAction         *m_toggleWWMarker;
     KToggleAction         *m_toggleNPSpaces;
+    KToggleAction         *m_toggleWordCount;
     QAction               *m_switchCmdLine;
     KToggleAction         *m_viInputModeAction;
 
diff --git a/src/view/wordcounter.cpp b/src/view/wordcounter.cpp
new file mode 100644
index 0000000..c5171f5
--- /dev/null
+++ b/src/view/wordcounter.cpp
@@ -0,0 +1,189 @@
+/* This file is part of the KDE libraries
+
+   This library is free software; you can redistribute it and/or
+   modify it under the terms of the GNU Library General Public
+   License version 2 as published by the Free Software Foundation.
+
+   This library is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+   Library General Public License for more details.
+
+   You should have received a copy of the GNU Library General Public License
+   along with this library; see the file COPYING.LIB.  If not, write to
+   the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+   Boston, MA 02111-13020, USA.
+*/
+
+#include "wordcounter.h"
+#include "katedocument.h"
+#include "kateview.h"
+
+namespace {
+    const int MaximumLinesToRecalculate = 100;
+}
+
+WordCounter::WordCounter(KTextEditor::ViewPrivate *view)
+    : QObject(view)
+    , m_wordsInDocument(0)
+    , m_wordsInSelection(0)
+    , m_charsInDocument(0)
+    , m_charsInSelection(0)
+    , m_startRecalculationFrom(0)
+    , m_document(view->document())
+{
+    connect(view->doc(), &KTextEditor::DocumentPrivate::textInserted, this, &WordCounter::textInserted);
+    connect(m_document, &KTextEditor::Document::textRemoved, this, &WordCounter::textRemoved);
+    connect(view->doc(), &KTextEditor::DocumentPrivate::loaded, this, &WordCounter::recalculate);
+    connect(view, &KTextEditor::View::selectionChanged, this, &WordCounter::selectionChanged);
+
+    m_timer.setInterval(500);
+    m_timer.setSingleShot(true);
+    connect(&m_timer, &QTimer::timeout, this, &WordCounter::recalculateLines);
+
+    recalculate(m_document);
+}
+
+void WordCounter::textInserted(KTextEditor::Document *, const KTextEditor::Range &range)
+{
+    const int startLine = range.start().line();
+    const int endLine = range.end().line();
+    int newLines = endLine - startLine;
+
+    if (m_countByLine.count() == 0) { // was empty document before insert
+        newLines++;
+    }
+
+    if (newLines > 0) {
+        m_countByLine.insert(startLine, newLines, -1);
+    }
+
+    m_countByLine[endLine] = -1;
+    m_timer.start();
+}
+
+void WordCounter::textRemoved(KTextEditor::Document *, const KTextEditor::Range &range, const QString &)
+{
+    const int startLine = range.start().line();
+    const int endLine = range.end().line();
+    const int removedLines = endLine - startLine;
+
+    if (removedLines > 0) {
+        m_countByLine.remove(startLine, removedLines);
+    }
+
+    if (m_countByLine.size() > 0) {
+        m_countByLine[startLine] = -1;
+        m_timer.start();
+    } else {
+        emit changed(0, 0, 0, 0);
+    }
+}
+
+void WordCounter::recalculate(KTextEditor::Document *)
+{
+    m_countByLine = QVector<int>(m_document->lines(), -1);
+    m_timer.start();
+}
+
+void WordCounter::selectionChanged(KTextEditor::View *view)
+{
+    if (view->selectionRange().isEmpty()) {
+        m_wordsInSelection = m_charsInSelection = 0;
+        emit changed(m_wordsInDocument, 0, m_charsInDocument, 0);
+        return;
+    }
+
+    const int firstLine = view->selectionRange().start().line();
+    const int lastLine = view->selectionRange().end().line();
+
+    if (firstLine == lastLine || view->blockSelection()) {
+        const QString text = view->selectionText();
+        m_wordsInSelection = countWords(text);
+        m_charsInSelection = text.size();
+    } else {
+        m_wordsInSelection = m_charsInSelection = 0;
+
+        const KTextEditor::Range firstLineRange(view->selectionRange().start(), firstLine, view->document()->lineLength(firstLine));
+        const QString firstLineText = view->document()->text(firstLineRange);
+        m_wordsInSelection += countWords(firstLineText);
+        m_charsInSelection += firstLineText.size();
+
+        // whole lines
+        for (int i = firstLine + 1; i < lastLine; i++) {
+            m_wordsInSelection += m_countByLine[i];
+            m_charsInSelection += m_document->lineLength(i);
+        }
+
+        const KTextEditor::Range lastLineRange(KTextEditor::Cursor(lastLine, 0), view->selectionRange().end());
+        const QString lastLineText = view->document()->text(lastLineRange);
+        m_wordsInSelection += countWords(lastLineText);
+        m_charsInSelection += lastLineText.size();
+    }
+
+    emit changed(m_wordsInDocument, m_wordsInSelection, m_charsInDocument, m_charsInSelection);
+}
+
+void WordCounter::recalculateLines()
+{
+    if (m_document->lines() == 0) {
+        m_countByLine.clear();
+        m_wordsInDocument = m_charsInSelection = 0;
+        return;
+    }
+
+    if (m_startRecalculationFrom >= m_countByLine.size()) {
+        m_startRecalculationFrom = 0;
+    }
+
+    int wordsCount = 0, charsCount = 0;
+    int calculated = 0;
+    int i = m_startRecalculationFrom;
+
+    forever {
+        if (m_countByLine[i] == -1) {
+            m_countByLine[i] = countWords(m_document->line(i));
+            if (++calculated > MaximumLinesToRecalculate) {
+                m_startRecalculationFrom = i;
+                m_timer.start();
+                return;
+            }
+        }
+
+        wordsCount += m_countByLine[i];
+        charsCount += m_document->lineLength(i);
+
+        if (++i == m_countByLine.size()) { // array cycle
+            i = 0;
+        }
+
+        if (i == m_startRecalculationFrom) {
+            break;
+        }
+    }
+
+    m_wordsInDocument = wordsCount;
+    m_charsInDocument = charsCount;
+    emit changed(m_wordsInDocument, m_wordsInSelection, m_charsInDocument, m_charsInSelection);
+}
+
+int WordCounter::countWords(const QString &text) const
+{
+    int count = 0;
+    bool inWord = false;
+
+    for (const QChar &c : text) {
+        if (c.isLetterOrNumber()) {
+            if (!inWord) {
+                inWord = true;
+            }
+        } else {
+            if (inWord) {
+                inWord = false;
+                count++;
+            }
+        }
+    }
+
+    return (inWord) ? count + 1: count;
+}
diff --git a/src/view/wordcounter.h b/src/view/wordcounter.h
new file mode 100644
index 0000000..39ad300
--- /dev/null
+++ b/src/view/wordcounter.h
@@ -0,0 +1,63 @@
+/* This file is part of the KDE libraries
+
+   This library is free software; you can redistribute it and/or
+   modify it under the terms of the GNU Library General Public
+   License version 2 as published by the Free Software Foundation.
+
+   This library is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+   Library General Public License for more details.
+
+   You should have received a copy of the GNU Library General Public License
+   along with this library; see the file COPYING.LIB.  If not, write to
+   the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+   Boston, MA 02110-1301, USA.
+*/
+
+#ifndef WORDCOUNTER_H
+#define WORDCOUNTER_H
+
+#include <QObject>
+#include <QVector>
+#include <QString>
+#include <QTimer>
+
+namespace KTextEditor {
+    class Document;
+    class DocumentPrivate;
+    class View;
+    class ViewPrivate;
+    class Range;
+}
+
+class WordCounter : public QObject
+{
+    Q_OBJECT
+
+public:
+    explicit WordCounter(KTextEditor::ViewPrivate *view);
+
+Q_SIGNALS:
+    void changed(int wordsInDocument, int wordsInSelection, int charsInDocument, int charsInSelection);
+
+private Q_SLOTS:
+    void textInserted(KTextEditor::Document *document, const KTextEditor::Range &range);
+    void textRemoved(KTextEditor::Document *document, const KTextEditor::Range &range, const QString &oldText);
+    void recalculate(KTextEditor::Document *document);
+    void selectionChanged(KTextEditor::View *view);
+    void recalculateLines();
+
+private:
+    int countWords(const QString &text) const;
+
+private:
+    QVector<int> m_countByLine;
+    int m_wordsInDocument, m_wordsInSelection;
+    int m_charsInDocument, m_charsInSelection;
+    QTimer m_timer;
+    int m_startRecalculationFrom;
+    KTextEditor::Document *m_document;
+};
+
+#endif


More information about the kde-doc-english mailing list