Skip to content

Commit

Permalink
Merge pull request #106 from Ruben-Sten/word-count
Browse files Browse the repository at this point in the history
Word count
  • Loading branch information
HannahSchellekens authored Aug 21, 2017
2 parents d8cc052 + 8bc5004 commit d05a8d3
Show file tree
Hide file tree
Showing 14 changed files with 352 additions and 25 deletions.
15 changes: 13 additions & 2 deletions resources/META-INF/plugin.xml
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@
text="Subp_aragraph" description="Insert the subparagraph command." />
</group>

<!-- Insert: Sectioning -->
<!-- Insert: Font Style -->
<group id="texify.LatexMenu.Insert.FontStyle" class="nl.rubensten.texifyidea.action.group.InsertFontStyleActionGroup" text="_Font Style" description="Insert font style commands." popup="true">
<add-to-group group-id="texify.LatexMenu" anchor="after" relative-to-action="texify.LatexMenu.Insert.Sectioning" />

Expand Down Expand Up @@ -163,8 +163,19 @@
</action>
</group>

<!-- Analysis -->
<group id="texify.LatexMenu.Analysis" class="nl.rubensten.texifyidea.action.group.AnalysisActionGroup" text="_Analysis" description="Analyse your documents." popup="true">
<add-to-group group-id="texify.LatexMenu" anchor="before" relative-to-action="texify.LatexMenu.Sumatra"/>

<action class="nl.rubensten.texifyidea.action.analysis.WordCountAction" id="texify.analysis.WordCount"
text="_Word count" description="Estimate the word count of the currently active .tex file and inclusions.">
<keyboard-shortcut first-keystroke="control alt W" keymap="$default" />
</action>
</group>

<!-- SumatraPDF -->
<group id="texify.LatexMenu.Sumatra" class="nl.rubensten.texifyidea.action.group.SumatraActionGroup" text="Sumatra_PDF" description="Interact with SumatraPDF." popup="true">
<group id="texify.LatexMenu.Sumatra" class="nl.rubensten.texifyidea.action.group.SumatraActionGroup" text="Sumatra_PDF"
description="Interact with SumatraPDF." popup="true" icon="/nl/rubensten/texifyidea/icons/sumatra.pdf">
<add-to-group group-id="texify.LatexMenu" anchor="before" relative-to-action="texify.ToggleStar"/>

<action class="nl.rubensten.texifyidea.action.sumatra.ForwardSearchAction" id="texify.sumatra.ForwardSearch"
Expand Down
7 changes: 7 additions & 0 deletions src/nl/rubensten/texifyidea/TexifyIcons.java
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,13 @@ public class TexifyIcons {
"/nl/rubensten/texifyidea/icons/font-smallcaps.png"
);

/**
* Copyright (c) 2017 Ruben Schellekens
*/
public static final Icon SUMATRA = IconLoader.getIcon(
"/nl/rubensten/texifyidea/icons/sumatra.png"
);

/**
* Get the file icon object that corresponds to the given file extension.
* <p>
Expand Down
230 changes: 230 additions & 0 deletions src/nl/rubensten/texifyidea/action/analysis/WordCountAction.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
package nl.rubensten.texifyidea.action.analysis

import com.intellij.icons.AllIcons
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.PlatformDataKeys
import com.intellij.openapi.ui.DialogBuilder
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
import com.intellij.psi.PsiWhiteSpace
import nl.rubensten.texifyidea.psi.*
import nl.rubensten.texifyidea.util.childrenOfType
import nl.rubensten.texifyidea.util.grandparent
import nl.rubensten.texifyidea.util.psiFile
import nl.rubensten.texifyidea.util.referencedFiles
import java.util.regex.Pattern
import javax.swing.JLabel
import javax.swing.SwingConstants

/**
* @author Ruben Schellekens
*/
open class WordCountAction : AnAction(
"Word Count",
"Estimate the word count of the currently active .tex file and inclusions.",
null
) {

companion object {

/**
* Commands that should be ignored by the word counter.
*/
private val IGNORE_COMMANDS = listOf(
"\\usepackage", "\\documentclass", "\\label", "\\linespread", "\\ref", "\\cite", "\\eqref", "\\nameref",
"\\autoref", "\\fullref", "\\pageref", "\\newcounter", "\\newcommand", "\\renewcommand",
"\\setcounter", "\\resizebox", "\\includegraphics", "\\include", "\\input", "\\refstepcounter",
"\\counterwithins"
)

/**
* Words that are contractions when `'s` is appended.
*/
private val CONTRACTION_S = listOf(
"that", "it", "there", "she", "he"
)

/**
* Characters that serve as delimiters for contractions.
*/
private val CONTRACTION_CHARACTERS = Pattern.compile("['’]")

/**
* Words containing solely of punctuation must be ignored.
*/
private val PUNCTUATION = Pattern.compile("[.,\\-_–:;?!'\"~=+*/\\\\&|]+")
}

override fun actionPerformed(event: AnActionEvent?) {
val virtualFile = event?.getData(PlatformDataKeys.VIRTUAL_FILE) ?: return
val project = event.getData(PlatformDataKeys.PROJECT) ?: return
val psiFile = virtualFile.psiFile(project) ?: return

val (words, chars) = countWords(psiFile)
val dialog = makeDialog(psiFile, words, chars)

dialog.show()
}

/**
* Builds the dialog that must show the word count.
*/
private fun makeDialog(baseFile: PsiFile, wordCount: Int, characters: Int): DialogBuilder {
return DialogBuilder().apply {
setTitle("Word count")

setCenterPanel(JLabel(
"""|<html>
|<p>Analysis of <i>${baseFile.name}</i> (and inclusions):</p>
|<table cellpadding=1 style='margin-top:4px'>
| <tr><td style='text-align:right'>Word count:</td><td><b>$wordCount</b></td></tr>
| <tr><td style='text-align:right'>Character count:</td><td><b>$characters</b></td>
|</table>
|</html>""".trimMargin(),
AllIcons.General.InformationDialog,
SwingConstants.LEADING
))

addOkAction()
setOkOperation {
dialogWrapper.close(0)
}
}
}

/**
* Counts all the words in the given base file.
*/
private fun countWords(baseFile: PsiFile): Pair<Int, Int> {
val fileSet = baseFile.referencedFiles()
val allNormalText = fileSet.flatMap { it.childrenOfType(LatexNormalText::class) }

val bibliographies = baseFile.childrenOfType(LatexEnvironment::class)
.filter {
val children = it.childrenOfType(LatexBeginCommand::class)
if (children.isEmpty()) {
return@filter false
}

val parameters = children.first().parameterList
if (parameters.isEmpty()) {
return@filter false
}

return@filter parameters[0].text == "{thebibliography}"
}
val bibliography = bibliographies.flatMap { it.childrenOfType(LatexNormalText::class) }

val (wordsNormal, charsNormal) = countWords(allNormalText)
val (wordsBib, charsBib) = countWords(bibliography)

return Pair(wordsNormal - wordsBib, charsNormal - charsBib)
}

/**
* Counts all the words in the text elements.
*
* @return A pair of the total amount of words, and the amount of characters that make up the words.
*/
private fun countWords(latexNormalText: List<LatexNormalText>): Pair<Int, Int> {
// Seperate all latex words.
val latexWords: MutableSet<PsiElement> = HashSet()
var characters: Int = 0
for (text in latexNormalText) {
var child = text.firstChild
while (child != null) {
latexWords.add(child)
child = child.nextSibling

if (child is PsiWhiteSpace) {
characters += child.textLength
child = child.nextSibling
continue
}
}
}

// Count words.
val filteredWords = filterWords(latexWords)

var words: Int = 0
for (word in filteredWords) {
words += contractionCount(word.text)
characters += word.textLength
}

return Pair(words, characters)
}

/**
* Filters out all the words that should not be counted.
*/
private fun filterWords(words: MutableSet<PsiElement>): Set<PsiElement> {
val set: MutableSet<PsiElement> = HashSet()

for (word in words) {
if (isWrongCommand(word) || isOptionalParameter(word) || isEnvironmentMarker(word) || isPunctuation(word)) {
continue
}

set.add(word)
}

return set
}

/**
* `I've` counts for two words: `I` and `have`. This function gives you the number of original words.
*/
private fun contractionCount(text: String): Int {
val split = CONTRACTION_CHARACTERS.split(text)
var count = 0
for (i in 0 until split.size) {
val string = split[i]

// Only count contractions: so do not count start or end single quotes :)
if (string.isEmpty()) {
continue
}

if (string.toLowerCase() == "s") {
if (split.size == 1) {
return 1
}

if (CONTRACTION_S.contains(split[i - 1].toLowerCase())) {
count++
}
}
else {
count++
}
}

return count
}

private fun isPunctuation(word: PsiElement): Boolean {
return PUNCTUATION.matcher(word.text).matches()
}

private fun isEnvironmentMarker(word: PsiElement): Boolean {
val grandparent = word.grandparent(7)
return grandparent is LatexBeginCommand || grandparent is LatexEndCommand
}

private fun isOptionalParameter(word: PsiElement): Boolean {
return word.grandparent(5) is LatexOptionalParam
}

private fun isWrongCommand(word: PsiElement): Boolean {
val command = word.grandparent(7) as? LatexCommands ?: return false

if (IGNORE_COMMANDS.contains(command.name)) {
return true
}

return false
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package nl.rubensten.texifyidea.action.group

import com.intellij.openapi.actionSystem.DefaultActionGroup

/**
* @author Ruben Schellekens
*/
open class AnalysisActionGroup : DefaultActionGroup() {
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package nl.rubensten.texifyidea.action.group

import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.DefaultActionGroup
import nl.rubensten.texifyidea.TexifyIcons

/**
* @author Ruben Schellekens
*/
open class InsertFontStyleActionGroup : DefaultActionGroup() {

override fun update(event: AnActionEvent?) {
super.update(event)
event?.presentation?.icon = TexifyIcons.FONT_ITALICS
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package nl.rubensten.texifyidea.action.group

import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.DefaultActionGroup
import nl.rubensten.texifyidea.TexifyIcons

/**
* @author Ruben Schellekens
*/
open class InsertSectioningActionGroup : DefaultActionGroup() {

override fun update(event: AnActionEvent?) {
super.update(event)
event?.presentation?.icon = TexifyIcons.DOT_SECTION
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
package nl.rubensten.texifyidea.action.group;

import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.actionSystem.DataContext;
import com.intellij.openapi.actionSystem.DefaultActionGroup;
import com.intellij.openapi.util.SystemInfo;
import nl.rubensten.texifyidea.TexifyIcons;

/**
* @author Sten Wessel
* @author Ruben Schellekens, Sten Wessel
*/
public class SumatraActionGroup extends DefaultActionGroup {

Expand All @@ -18,4 +20,10 @@ public boolean canBePerformed(DataContext context) {
public boolean hideIfNoVisibleChildren() {
return true;
}

@Override
public void update(AnActionEvent event) {
super.update(event);
event.getPresentation().setIcon(TexifyIcons.SUMATRA);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import javax.swing.SwingConstants
* @author Sten Wessel
* @since b0.4
*/
class ConfigureInverseSearchAction : AnAction("ConfigureInverseSearch") {
open class ConfigureInverseSearchAction : AnAction("ConfigureInverseSearch") {

override fun actionPerformed(e: AnActionEvent?) {
DialogBuilder().apply {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import nl.rubensten.texifyidea.run.SumatraConversation
* @author Sten Wessel
* @since b0.4
*/
class ForwardSearchAction : EditorAction("ForwardSearch", null) {
open class ForwardSearchAction : EditorAction("ForwardSearch", null) {

override fun actionPerformed(file: VirtualFile, project: Project, editor: TextEditor) {
if (!SystemInfo.isWindows) {
Expand Down
11 changes: 11 additions & 0 deletions src/nl/rubensten/texifyidea/util/FileUtil.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package nl.rubensten.texifyidea.util

import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.PsiFile
import com.intellij.psi.PsiManager

/**
* Looks up the PsiFile that corresponds to the Virtual File.
*/
fun VirtualFile.psiFile(project: Project): PsiFile? = PsiManager.getInstance(project).findFile(this)
Loading

0 comments on commit d05a8d3

Please sign in to comment.