Skip to content

Commit 272f533

Browse files
committed
feat: enhance inspections, tests, and parser/lexer integration
1 parent 4448fc5 commit 272f533

18 files changed

Lines changed: 308 additions & 75 deletions

build.gradle.kts

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import org.jetbrains.changelog.markdownToHTML
2+
import org.jetbrains.grammarkit.tasks.GenerateLexerTask
3+
import org.jetbrains.grammarkit.tasks.GenerateParserTask
24
import org.jetbrains.intellij.platform.gradle.TestFrameworkType
35

46
plugins {
@@ -8,14 +10,19 @@ plugins {
810
alias(libs.plugins.changelog) // Gradle Changelog Plugin
911
alias(libs.plugins.qodana) // Gradle Qodana Plugin
1012
alias(libs.plugins.kover) // Gradle Kover Plugin
13+
alias(libs.plugins.grammarKit) // GrammarKit/JFlex generation
1114
}
1215

1316
group = providers.gradleProperty("pluginGroup").get()
1417
version = providers.gradleProperty("pluginVersion").get()
18+
19+
val generatedParserDir = layout.buildDirectory.dir("generated-src/grammarKit/parser")
20+
val generatedLexerDir = layout.buildDirectory.dir("generated-src/grammarKit/lexer")
21+
1522
sourceSets {
1623
main {
1724
java {
18-
srcDirs("src/main/java", "src/main/gen")
25+
srcDirs("src/main/java", generatedParserDir, generatedLexerDir)
1926
}
2027
}
2128
}
@@ -111,6 +118,28 @@ kover {
111118
}
112119

113120
tasks {
121+
generateParser {
122+
sourceFile.set(layout.projectDirectory.file("src/main/kotlin/com/github/xepozz/crontab/language/parser/Crontab.bnf"))
123+
targetRootOutputDir.set(generatedParserDir)
124+
pathToParser.set("com/github/xepozz/crontab/language/parser/CrontabParser.java")
125+
pathToPsiRoot.set("com/github/xepozz/crontab/language/psi")
126+
purgeOldFiles.set(true)
127+
}
128+
129+
generateLexer {
130+
sourceFile.set(layout.projectDirectory.file("src/main/kotlin/com/github/xepozz/crontab/language/parser/Crontab.flex"))
131+
targetOutputDir.set(generatedLexerDir.map { it.dir("com/github/xepozz/crontab/language/parser") })
132+
purgeOldFiles.set(true)
133+
}
134+
135+
withType<JavaCompile>().configureEach {
136+
dependsOn(withType<GenerateParserTask>(), withType<GenerateLexerTask>())
137+
}
138+
139+
compileKotlin {
140+
dependsOn(withType<GenerateParserTask>(), withType<GenerateLexerTask>())
141+
}
142+
114143
wrapper {
115144
gradleVersion = providers.gradleProperty("gradleVersion").get()
116145
}

gradle/libs.versions.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ intelliJPlatform = "2.11.0"
88
kotlin = "2.3.0"
99
kover = "0.9.4"
1010
qodana = "2025.3.1"
11+
grammarKit = "2023.3.0.3"
1112

1213
[libraries]
1314
junit = { group = "junit", name = "junit", version.ref = "junit" }
@@ -18,3 +19,4 @@ intelliJPlatform = { id = "org.jetbrains.intellij.platform", version.ref = "inte
1819
kotlin = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
1920
kover = { id = "org.jetbrains.kotlinx.kover", version.ref = "kover" }
2021
qodana = { id = "org.jetbrains.qodana", version.ref = "qodana" }
22+
grammarKit = { id = "org.jetbrains.grammarkit", version.ref = "grammarKit" }

src/main/kotlin/com/github/xepozz/crontab/ide/CrontabInlayHintsProvider.kt

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,14 +35,13 @@ class CrontabInlayHintsProvider : InlayHintsProvider {
3535
val text = QUOTES.fold(element.text) { acc, string -> StringUtil.unquoteString(acc, string) }
3636

3737
val crontabFile = CrontabElementFactory.createFile(element.project, text)
38-
val expression = crontabFile.children.getOrNull(0) as? CrontabCronExpression ?: return
38+
val expression = PsiTreeUtil.findChildOfType(crontabFile, CrontabCronExpression::class.java) ?: return
3939
when {
4040
expression.schedule.text != text -> return
4141
text.startsWith("@") -> return
4242
}
4343

4444
val schedules = PsiTreeUtil.findChildrenOfType(crontabFile, CrontabSchedule::class.java)
45-
// println("schedules: ${schedules}")
4645

4746
if (schedules.isNotEmpty()) {
4847
val text = CronScheduleDescriber.asHumanReadable(text).ifEmpty { return }
@@ -60,4 +59,4 @@ class CrontabInlayHintsProvider : InlayHintsProvider {
6059
}
6160
}
6261

63-
}
62+
}

src/main/kotlin/com/github/xepozz/crontab/ide/CrontabRunCommandMarkerContributor.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ import com.intellij.psi.PsiElement
99
internal class CrontabRunCommandMarkerContributor : RunLineMarkerContributor() {
1010
override fun getInfo(leaf: PsiElement): Info? {
1111
val element = leaf.parent as? CrontabCommand ?: return null
12+
if (element.text.isBlank()) return null
1213

1314
return Info(AllIcons.Actions.Execute, arrayOf(CrontabRunCommandAction(element.text))) { "Run in Terminal" }
1415
}
15-
}
16+
}
Lines changed: 37 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,26 @@
11
package com.github.xepozz.crontab.ide
22

33
import com.github.xepozz.crontab.language.psi.CrontabTimeList
4+
import com.github.xepozz.crontab.language.psi.impl.CrontabImplUtil
45

56
object CrontabTimeRangeUtil {
6-
fun expandRanges(element: CrontabTimeList): Set<Int> {
7-
val input = element.timeListItemList
8-
.mapNotNull {
9-
when {
10-
it.timeRange != null -> "${it.timeRange!!.text}"
11-
it.timeExactNumber != null -> "${it.timeExactNumber!!.text}"
12-
else -> null
13-
}
7+
fun canCollapseRanges(element: CrontabTimeList): Boolean {
8+
return element.timeListItemList.all {
9+
val range = it.timeRange
10+
when {
11+
it.timeExactNumber != null -> true
12+
range != null -> CrontabImplUtil.getFirst(range) <= CrontabImplUtil.getSecond(range)
13+
else -> false
1414
}
15-
return expandRanges(input)
15+
}
16+
}
17+
18+
fun expandRanges(element: CrontabTimeList): Set<Int> {
19+
return expandRanges(element.numericItems())
1620
}
1721

1822
fun collapseRanges(element: CrontabTimeList): List<String> {
19-
val input = element.timeListItemList
20-
.mapNotNull {
21-
when {
22-
it.timeRange != null -> "${it.timeRange!!.text}"
23-
it.timeExactNumber != null -> "${it.timeExactNumber!!.text}"
24-
else -> null
25-
}
26-
}
27-
return collapseRanges(input)
23+
return collapseRanges(element.numericItems())
2824
}
2925

3026
fun collapseRanges(input: List<String>): List<String> {
@@ -37,28 +33,29 @@ object CrontabTimeRangeUtil {
3733
var rangeStart = sortedNumbers[0]
3834
var rangeEnd = sortedNumbers[0]
3935

36+
fun addRange(start: Int, end: Int) {
37+
when {
38+
start == end -> result.add("$start")
39+
start + 1 == end -> {
40+
result.add("$start")
41+
result.add("$end")
42+
}
43+
44+
else -> result.add("$start-$end")
45+
}
46+
}
47+
4048
for (i in 1 until sortedNumbers.size) {
4149
if (sortedNumbers[i] == rangeEnd + 1) {
4250
rangeEnd = sortedNumbers[i]
4351
} else {
44-
if (rangeStart == rangeEnd) {
45-
result.add("$rangeStart")
46-
} else {
47-
result.add("$rangeStart-$rangeEnd")
48-
}
52+
addRange(rangeStart, rangeEnd)
4953
rangeStart = sortedNumbers[i]
5054
rangeEnd = sortedNumbers[i]
5155
}
5256
}
5357

54-
// Add the last range
55-
if (rangeStart == rangeEnd) {
56-
result.add("$rangeStart")
57-
} else if (rangeStart + 1 == rangeEnd) {
58-
result.add("$rangeStart,$rangeEnd")
59-
} else {
60-
result.add("$rangeStart-$rangeEnd")
61-
}
58+
addRange(rangeStart, rangeEnd)
6259

6360
return result
6461
}
@@ -69,17 +66,22 @@ object CrontabTimeRangeUtil {
6966

7067
// Parse the input and collect all individual numbers and ranges
7168
for (item in input) {
72-
if (item.contains("-")) {
73-
// Handle range
69+
if ("-" in item) {
7470
val parts = item.split("-")
71+
if (parts.size != 2) continue
7572
val start = parts[0].toIntOrNull() ?: continue
7673
val end = parts[1].toIntOrNull() ?: continue
7774
numbers.addAll(start..end)
7875
} else {
79-
// Handle individual number
8076
numbers.add(item.toIntOrNull() ?: continue)
8177
}
8278
}
8379
return numbers
8480
}
85-
}
81+
82+
private fun CrontabTimeList.numericItems(): List<String> {
83+
return timeListItemList.mapNotNull {
84+
it.timeRange?.text ?: it.timeExactNumber?.text
85+
}
86+
}
87+
}

src/main/kotlin/com/github/xepozz/crontab/ide/actions/CrontabGuruIntention.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ class CrontabGuruIntention : PsiElementBaseIntentionAction(), Iconable, DumbAwar
2727
}
2828

2929
override fun isAvailable(project: Project, editor: Editor?, element: PsiElement): Boolean {
30-
return element.containingFile is CrontabFile
30+
return element.containingFile is CrontabFile && CrontabPsiTreeUtils.findCrontabSchedule(element) != null
3131
}
3232

3333
override fun generatePreview(project: Project, editor: Editor, file: PsiFile) = IntentionPreviewInfo.EMPTY
@@ -37,4 +37,4 @@ class CrontabGuruIntention : PsiElementBaseIntentionAction(), Iconable, DumbAwar
3737
override fun getText() = "Open in crontab.guru"
3838

3939
override fun getIcon(flags: Int) = CrontabIcons.GURU
40-
}
40+
}

src/main/kotlin/com/github/xepozz/crontab/ide/actions/OpenCrontabGuruAction.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ class OpenCrontabGuruAction : AnAction() {
2222
}
2323

2424
override fun update(event: AnActionEvent) {
25-
println("call update $event")
25+
event.presentation.isEnabledAndVisible = false
26+
2627
val editor = event.getData(CommonDataKeys.EDITOR) ?: return
2728
val psiFile = event.getData(CommonDataKeys.PSI_FILE) as? CrontabFile ?: return
2829
val psiElement = psiFile.findElementAt(editor.caretModel.offset) ?: return
@@ -33,4 +34,4 @@ class OpenCrontabGuruAction : AnAction() {
3334
}
3435

3536
override fun getActionUpdateThread() = ActionUpdateThread.BGT
36-
}
37+
}

src/main/kotlin/com/github/xepozz/crontab/ide/documentation/CrontabDocumentationProvider.kt

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ class CrontabDocumentationProvider : DocumentationProvider {
5151
is CrontabSchedule -> {
5252
val file = SymbolPresentationUtil.getFilePathPresentation(element.containingFile)
5353
val cronExpression = element.parent as? CrontabCronExpression
54-
val command = cronExpression?.children[1] as? CrontabCommand
54+
val command = cronExpression?.command
5555
val docComment = CrontabDocumentationUtils.findCrontabElementDocumentation(cronExpression)
5656

5757
renderFullDoc(element, command, file, docComment, element.project)
@@ -60,7 +60,7 @@ class CrontabDocumentationProvider : DocumentationProvider {
6060
is CrontabVariableName -> {
6161
val file = SymbolPresentationUtil.getFilePathPresentation(element.containingFile)
6262
val variableDefinition = element.parent as? CrontabVariableDefinition
63-
val value = variableDefinition?.children[1] as? CrontabVariableValue
63+
val value = variableDefinition?.variableValue
6464
val docComment = CrontabDocumentationUtils.findCrontabElementDocumentation(variableDefinition)
6565

6666
renderFullDoc(element, value, file, docComment, element.project)
@@ -73,15 +73,9 @@ class CrontabDocumentationProvider : DocumentationProvider {
7373
override fun getQuickNavigateInfo(element: PsiElement?, originalElement: PsiElement?): String? {
7474
return when (element) {
7575
is CrontabSchedule -> CronScheduleDescriber.asHumanReadable(element.text)
76-
is CrontabVariableName -> {
77-
val parent = element.parent as CrontabVariableDefinition
78-
79-
"${parent.variableValue}"
80-
}
81-
82-
else -> "element.text: ${element?.text}"
76+
is CrontabVariableName -> (element.parent as? CrontabVariableDefinition)?.variableValue?.text
77+
else -> element?.text
8378
}
84-
return null
8579
}
8680

8781
override fun generateRenderedDoc(comment: PsiDocCommentBase) = markdownToHtml(comment.text, comment.project)
@@ -186,4 +180,4 @@ class CrontabDocumentationProvider : DocumentationProvider {
186180
append(DocumentationMarkup.SECTION_END)
187181
append(DocumentationMarkup.SECTIONS_END)
188182
}
189-
}
183+
}

0 commit comments

Comments
 (0)