Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions .github/workflows/pr-check.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
name: PR Check

on:
pull_request:
branches: [ main, master ]

jobs:
check:
name: ktlint and unit tests
runs-on: ubuntu-latest
steps:
- name: Fetch Sources
uses: actions/checkout@v4

- name: Setup Java
uses: actions/setup-java@v4
with:
distribution: 'zulu'
java-version: '21'

- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4

- name: Run Ktlint Check
run: ./gradlew ktlintCheck

- name: Run Unit Tests
run: ./gradlew test
50 changes: 39 additions & 11 deletions build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import org.jetbrains.intellij.platform.gradle.TestFrameworkType

plugins {
id("java")
id("org.jetbrains.kotlin.jvm") version "1.9.22"
id("org.jetbrains.intellij.platform") version "2.1.0"
id("org.jlleitschuh.gradle.ktlint") version "12.1.2"
}

version = "1.0.3"
version = "1.0.4"

group = "com.github.kiolk.typingplugin"

Expand All @@ -20,30 +23,45 @@ dependencies {
intellijIdeaCommunity("2024.3")
bundledPlugins("com.intellij.java")
instrumentationTools()
testFramework(TestFrameworkType.Platform)
zipSigner()
}
testImplementation("org.junit.jupiter:junit-jupiter-api:5.10.2")
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.10.2")
testRuntimeOnly("org.junit.vintage:junit-vintage-engine:5.10.2")
testImplementation("junit:junit:4.13.2")
}

intellijPlatform {
pluginConfiguration {
name = "Typing Training"
description =
"""
Improved stability and usability.
- Added ability to drag typing dialog to any position on the screen.
- Improved stability.
""".trimIndent()
changeNotes =
"""
- Added ability to drag typing dialog to any position on the screen.
- Improved stability.
""".trimIndent()
ideaVersion {
sinceBuild = "241"
untilBuild = "253.*"
// untilBuild is omitted to allow compatibility with all future versions
// and avoid "made-up" build number rejections.
}
}

signing {
// Robust handling for both literal newlines and the "\n" string
val cert = providers.environmentVariable("CERTIFICATE_CHAIN")
.orElse(providers.gradleProperty("certificateChain"))
.map { it.replace("\\n", "\n") }

val key = providers.environmentVariable("PRIVATE_KEY")
.orElse(providers.gradleProperty("privateKey"))
.map { it.replace("\\n", "\n") }
val cert =
providers.environmentVariable("CERTIFICATE_CHAIN")
.orElse(providers.gradleProperty("certificateChain"))
.map { it.replace("\\n", "\n") }

val key =
providers.environmentVariable("PRIVATE_KEY")
.orElse(providers.gradleProperty("privateKey"))
.map { it.replace("\\n", "\n") }

certificateChain.set(cert)
privateKey.set(key)
Expand All @@ -59,3 +77,13 @@ intellijPlatform {
kotlin {
jvmToolchain(21)
}

ktlint {
verbose.set(true)
outputToConsole.set(true)
coloredOutput.set(true)
}

tasks.test {
useJUnitPlatform()
}
Original file line number Diff line number Diff line change
@@ -1,35 +1,48 @@
package com.github.kiolk.typingplugin.actions

import com.github.kiolk.typingplugin.ui.TypingDialog
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.CommonDataKeys
import com.github.kiolk.typingplugin.ui.TypingDialog
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.project.Project

class StartTypingAction : AnAction() {
override fun actionPerformed(e: AnActionEvent) {
val project = e.project ?: return
val editor = e.getData(CommonDataKeys.EDITOR) ?: return

// Check for selection
val selectionModel = editor.selectionModel
val textToType = if (selectionModel.hasSelection()) {
selectionModel.selectedText ?: editor.document.text
} else {
editor.document.text
}

// Clean up the selected text:
// If we select a middle block, we might want to trim initial common indentation
val cleanedText = textToType.trimIndent()

val typingDialog = TypingDialog(project, cleanedText)

val cleanedText = getCleanedText(editor)

val typingDialog = createTypingDialog(project, cleanedText)
typingDialog.show()
}

fun getCleanedText(editor: Editor): String {
val selectionModel = editor.selectionModel
val textToType =
if (selectionModel.hasSelection()) {
selectionModel.selectedText ?: editor.document.text
} else {
editor.document.text
}

// Clean up the selected text:
// If we select a middle block, we might want to trim initial common indentation
return textToType.trimIndent()
}

fun createTypingDialog(
project: Project,
text: String,
): TypingDialog {
return TypingDialog(project, text)
}

override fun update(e: AnActionEvent) {
val editor = e.getData(CommonDataKeys.EDITOR)
e.presentation.isEnabledAndVisible = editor != null

// Optional: Update text based on selection
if (editor?.selectionModel?.hasSelection() == true) {
e.presentation.text = "Type Selected Area"
Expand Down
148 changes: 97 additions & 51 deletions src/main/kotlin/com/github/kiolk/typingplugin/ui/TypingDialog.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,52 +8,85 @@ import com.intellij.openapi.ui.Messages
import com.intellij.ui.components.JBScrollPane
import java.awt.BorderLayout
import java.awt.Color
import java.awt.Point
import java.awt.event.KeyAdapter
import java.awt.event.KeyEvent
import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent
import javax.swing.JComponent
import javax.swing.JPanel
import javax.swing.JTextPane
import javax.swing.SwingUtilities
import javax.swing.text.SimpleAttributeSet
import javax.swing.text.StyleConstants

class TypingDialog(private val project: Project, private val sourceCode: String) : DialogWrapper(project) {

private var currentIndex = 0
private var errorCount = 0
private var startTime: Long = 0
private val textPane = JTextPane()
private val skippedIndices = mutableSetOf<Int>()

// Styles
private val ghostAttributes = SimpleAttributeSet().apply {
StyleConstants.setForeground(this, Color.GRAY)
}
private val correctAttributes = SimpleAttributeSet().apply {
StyleConstants.setForeground(this, Color.GREEN)
}
private val wrongAttributes = SimpleAttributeSet().apply {
StyleConstants.setForeground(this, Color.RED)
StyleConstants.setUnderline(this, true)
}
private val cursorAttributes = SimpleAttributeSet().apply {
StyleConstants.setBackground(this, Color.LIGHT_GRAY)
StyleConstants.setForeground(this, Color.BLACK)
}
private val ghostAttributes =
SimpleAttributeSet().apply {
StyleConstants.setForeground(this, Color.GRAY)
}
private val correctAttributes =
SimpleAttributeSet().apply {
StyleConstants.setForeground(this, Color.GREEN)
}
private val wrongAttributes =
SimpleAttributeSet().apply {
StyleConstants.setForeground(this, Color.RED)
StyleConstants.setUnderline(this, true)
}
private val cursorAttributes =
SimpleAttributeSet().apply {
StyleConstants.setBackground(this, Color.LIGHT_GRAY)
StyleConstants.setForeground(this, Color.BLACK)
}

init {
title = "Typing Training"
isModal = false
init()
}

override fun getDimensionServiceKey(): String? = "com.github.kiolk.typingplugin.ui.TypingDialog"

override fun createCenterPanel(): JComponent {
val panel = JPanel(BorderLayout())


val dragListener =
object : MouseAdapter() {
private var initialScreenClick: Point? = null
private var initialWindowLocation: Point? = null

override fun mousePressed(e: MouseEvent) {
initialScreenClick = e.locationOnScreen
initialWindowLocation = SwingUtilities.getWindowAncestor(panel)?.location
}

override fun mouseDragged(e: MouseEvent) {
val window = SwingUtilities.getWindowAncestor(panel)
if (window != null && initialScreenClick != null && initialWindowLocation != null) {
val deltaX = e.locationOnScreen.x - initialScreenClick!!.x
val deltaY = e.locationOnScreen.y - initialScreenClick!!.y
window.setLocation(initialWindowLocation!!.x + deltaX, initialWindowLocation!!.y + deltaY)
}
}
}

panel.addMouseListener(dragListener)
panel.addMouseMotionListener(dragListener)

textPane.apply {
text = sourceCode
isEditable = false
background = EditorColorsManager.getInstance().globalScheme.defaultBackground
font = EditorColorsManager.getInstance().globalScheme.getFont(EditorFontType.PLAIN)

StyleConstants.setBackground(ghostAttributes, background)
StyleConstants.setBackground(correctAttributes, background)
StyleConstants.setBackground(wrongAttributes, background)
Expand All @@ -62,30 +95,37 @@ class TypingDialog(private val project: Project, private val sourceCode: String)
textPane.styledDocument.setCharacterAttributes(0, sourceCode.length, ghostAttributes, true)
updateCursor()

addKeyListener(object : KeyAdapter() {
override fun keyTyped(e: KeyEvent) {
if (e.keyChar.code < 32 || e.keyChar.code == 127) return
if (startTime == 0L) startTime = System.currentTimeMillis()
handleTyping(e.keyChar)
}
addKeyListener(
object : KeyAdapter() {
override fun keyTyped(e: KeyEvent) {
if (e.keyChar.code < 32 || e.keyChar.code == 127) return
if (startTime == 0L) startTime = System.currentTimeMillis()
handleTyping(e.keyChar)
}

override fun keyPressed(e: KeyEvent) {
when (e.keyCode) {
KeyEvent.VK_BACK_SPACE, KeyEvent.VK_DELETE, KeyEvent.VK_CLEAR -> {
handleBackspace()
}
KeyEvent.VK_ENTER -> {
if (startTime == 0L) startTime = System.currentTimeMillis()
handleTyping('\n')
override fun keyPressed(e: KeyEvent) {
when (e.keyCode) {
KeyEvent.VK_BACK_SPACE, KeyEvent.VK_DELETE, KeyEvent.VK_CLEAR -> {
handleBackspace()
}
KeyEvent.VK_ENTER -> {
if (startTime == 0L) startTime = System.currentTimeMillis()
handleTyping('\n')
}
}
}
}
})
},
)
addMouseListener(dragListener)
addMouseMotionListener(dragListener)
}

panel.add(JBScrollPane(textPane), BorderLayout.CENTER)
val scrollPane = JBScrollPane(textPane)
scrollPane.addMouseListener(dragListener)
scrollPane.addMouseMotionListener(dragListener)
panel.add(scrollPane, BorderLayout.CENTER)
panel.preferredSize = java.awt.Dimension(800, 600)

return panel
}

Expand All @@ -98,11 +138,11 @@ class TypingDialog(private val project: Project, private val sourceCode: String)

val targetChar = sourceCode[currentIndex]
val isNewlineMatch = (targetChar == '\n' || targetChar == '\r') && (charTyped == '\n' || charTyped == '\r')

if (charTyped == targetChar || isNewlineMatch) {
textPane.styledDocument.setCharacterAttributes(currentIndex, 1, correctAttributes, true)
if (targetChar == '\r' && currentIndex + 1 < sourceCode.length && sourceCode[currentIndex+1] == '\n') {

if (targetChar == '\r' && currentIndex + 1 < sourceCode.length && sourceCode[currentIndex + 1] == '\n') {
currentIndex += 2
} else {
currentIndex++
Expand Down Expand Up @@ -166,15 +206,16 @@ class TypingDialog(private val project: Project, private val sourceCode: String)
if (currentIndex < sourceCode.length) {
val attrs = textPane.styledDocument.getCharacterElement(currentIndex).attributes
val isWrong = StyleConstants.getForeground(attrs) == Color.RED

val style = if (isWrong) {
val s = SimpleAttributeSet(wrongAttributes)
StyleConstants.setBackground(s, Color.LIGHT_GRAY)
s
} else {
cursorAttributes
}


val style =
if (isWrong) {
val s = SimpleAttributeSet(wrongAttributes)
StyleConstants.setBackground(s, Color.LIGHT_GRAY)
s
} else {
cursorAttributes
}

textPane.styledDocument.setCharacterAttributes(currentIndex, 1, style, true)
textPane.caretPosition = currentIndex
}
Expand All @@ -186,11 +227,16 @@ class TypingDialog(private val project: Project, private val sourceCode: String)
val totalTimeMinutes = totalTimeSeconds / 60.0
val totalChars = sourceCode.length
val wpm = if (totalTimeMinutes > 0) (totalChars / 5.0) / totalTimeMinutes else 0.0
val accuracy = if (totalChars + errorCount > 0) {
(totalChars.toDouble() / (totalChars + errorCount)) * 100
} else 0.0
val accuracy =
if (totalChars + errorCount > 0) {
(totalChars.toDouble() / (totalChars + errorCount)) * 100
} else {
0.0
}

val statsMessage = "Typing Finished!\n\nTime: ${"%.1f".format(totalTimeSeconds)}s\nWPM: ${"%.1f".format(wpm)}\nAccuracy: ${"%.1f".format(accuracy)}%\nErrors: $errorCount"
val statsMessage = "Typing Finished!\n\nTime: ${"%.1f".format(
totalTimeSeconds,
)}s\nWPM: ${"%.1f".format(wpm)}\nAccuracy: ${"%.1f".format(accuracy)}%\nErrors: $errorCount"
Messages.showInfoMessage(project, statsMessage, "Session Summary")
}
}
Loading