Skip to content

Commit

Permalink
Merge pull request #21 from dinbtechit/feature/#20-CodeInsights-Quick…
Browse files Browse the repository at this point in the history
…fix-Action

fix for #20
  • Loading branch information
dinbtechit committed Sep 18, 2023
2 parents 1b0c459 + 34c3a93 commit 5faef59
Show file tree
Hide file tree
Showing 11 changed files with 330 additions and 22 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,14 @@
# ngxs Changelog

## [Unreleased]
### Added
- #20 - Code insights/Quickfix when an Action has no implementation in the *.state.ts

### Fixed
- Duplicate Actions will not show in gutter

### Changed
- ActionIcon - increased size.

## [0.0.3] - 2023-09-08

Expand Down
4 changes: 2 additions & 2 deletions gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ pluginGroup = com.github.dinbtechit.ngxs
pluginName = ngxs
pluginRepositoryUrl = https://github.com/dinbtechit/ngxs
# SemVer format -> https://semver.org
pluginVersion = 0.0.3
pluginVersion = 0.0.4

# Supported build number ranges and IntelliJ Platform versions -> https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html
pluginSinceBuild = 223
Expand All @@ -16,7 +16,7 @@ platformVersion = 2022.3.3

# Plugin Dependencies -> https://plugins.jetbrains.com/docs/intellij/plugin-dependencies.html
# Example: platformPlugins = com.intellij.java, com.jetbrains.php:203.4449.22
platformPlugins = JavaScript, PsiViewer:2022.3
platformPlugins = JavaScript, PsiViewer:2022.3, com.github.dinbtechit.vscodetheme:1.10.2

# Gradle Releases -> https://github.com/gradle/gradle/releases
gradleVersion = 8.3
Expand Down
2 changes: 1 addition & 1 deletion src/main/kotlin/com/github/dinbtechit/ngxs/NgxsIcons.kt
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,6 @@ object NgxsIcons {
object Gutter {
@JvmField
val Action = IconLoader.getIcon("icons/ngxs-action.svg", javaClass)
val MutipleActions = IconLoader.getIcon("icons/ngxs-multiple-action.svg", javaClass)
val MultipleActions = IconLoader.getIcon("icons/ngxs-multiple-action.svg", javaClass)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import com.github.dinbtechit.ngxs.NgxsIcons
import com.intellij.codeInsight.daemon.GutterIconNavigationHandler
import com.intellij.codeInsight.daemon.LineMarkerInfo
import com.intellij.codeInsight.daemon.LineMarkerProvider
import com.intellij.lang.ecmascript6.psi.impl.ES6FieldStatementImpl
import com.intellij.lang.javascript.psi.JSReferenceExpression
import com.intellij.lang.javascript.psi.ecma6.ES6Decorator
import com.intellij.lang.javascript.types.TypeScriptNewExpressionElementType
Expand Down Expand Up @@ -31,9 +32,12 @@ import javax.swing.JComponent
class NgxsActionLineMarkerIconProvider : LineMarkerProvider {
override fun getLineMarkerInfo(element: PsiElement): LineMarkerInfo<PsiElement>? {

if (element.parent is JSReferenceExpression
&& element.parent.parent.elementType is TypeScriptNewExpressionElementType
&& element.parent.reference?.resolve()?.containingFile?.name?.contains(".actions.ts") == true
if ((element.parent is JSReferenceExpression
&& element.parent.parent.elementType is TypeScriptNewExpressionElementType
&& element.parent.reference?.resolve() !== null) &&
element.parent.reference?.resolve()!!.children.any {
it is ES6FieldStatementImpl && it.text.contains("^static(.*)type".toRegex())
}
) {

val lineNumber = getLineNumber(element)
Expand All @@ -48,7 +52,7 @@ class NgxsActionLineMarkerIconProvider : LineMarkerProvider {

if (elements.first() != element) return null

val icon = if (navigateToElements.size > 1) NgxsIcons.Gutter.MutipleActions
val icon = if (navigateToElements.size > 1) NgxsIcons.Gutter.MultipleActions
else NgxsIcons.Gutter.Action

val tooltipText = if (navigateToElements.size > 1) "NGXS Multiple Actions"
Expand All @@ -74,7 +78,7 @@ class NgxsActionLineMarkerIconProvider : LineMarkerProvider {

return GutterIconNavigationHandler<PsiElement> { e, _ ->
val group = DefaultActionGroup()
for (navElement in navigateToElements) {
for (navElement in navigateToElements.distinctBy { it.text }) {
val action = object : AnAction({ "NGXS Action \"${navElement.text}\"" }, NgxsIcons.Gutter.Action) {
override fun actionPerformed(e: AnActionEvent) {
navigateToElement(navElement)
Expand Down Expand Up @@ -112,8 +116,8 @@ class NgxsActionLineMarkerIconProvider : LineMarkerProvider {
for (ref in refs.toList()) {
val actionDecoratorElement = PsiTreeUtil.findFirstParent(ref.element) { it is ES6Decorator }
val hasActionDecorator = actionDecoratorElement != null
if (hasActionDecorator &&
ref.element.containingFile.name.contains(".state.ts")
if (hasActionDecorator
&& ref.element.containingFile.name.contains(".state.ts")
) {
val fileEditorManager = FileEditorManager.getInstance(element.project)
val textEditor = fileEditorManager.openTextEditor(
Expand All @@ -124,7 +128,7 @@ class NgxsActionLineMarkerIconProvider : LineMarkerProvider {
)
val start = ref.element.textRange.startOffset
textEditor?.caretModel?.moveToOffset(start)
textEditor?.scrollingModel?.scrollToCaret(ScrollType.MAKE_VISIBLE)
textEditor?.scrollingModel?.scrollToCaret(ScrollType.CENTER)
}
}
}
Expand Down Expand Up @@ -152,7 +156,9 @@ class NgxsActionLineMarkerIconProvider : LineMarkerProvider {
if (element != null) {
if (element.parent is JSReferenceExpression
&& element.parent.parent.elementType is TypeScriptNewExpressionElementType
&& element.parent.reference?.resolve()?.containingFile?.name?.contains(".actions.ts") == true
&& element.parent.reference?.resolve()!!.children.any {
it is ES6FieldStatementImpl && it.text.contains("^static(.*)type".toRegex())
}
) {
res.add(element)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package com.github.dinbtechit.ngxs.action.editor

import com.intellij.lang.ecmascript6.psi.impl.ES6FieldStatementImpl
import com.intellij.lang.javascript.psi.JSReferenceExpression
import com.intellij.lang.javascript.psi.ecma6.ES6Decorator
import com.intellij.lang.javascript.types.TypeScriptClassElementType
import com.intellij.lang.javascript.types.TypeScriptNewExpressionElementType
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiWhiteSpace
import com.intellij.psi.search.searches.ReferencesSearch
import com.intellij.psi.util.PsiTreeUtil
import com.intellij.psi.util.elementType
import com.intellij.psi.util.nextLeafs

object NgxsActionUtil {

fun isActionDispatched(element: PsiElement): Boolean {
return (element.parent is JSReferenceExpression
&& element.parent.parent.elementType is TypeScriptNewExpressionElementType
&& element.parent.reference?.resolve() !== null) &&
element.parent.reference?.resolve()!!.children.any {
it is ES6FieldStatementImpl && it.text.contains("^static(.*)type".toRegex())
}
}

fun isActionClass(element: PsiElement): Boolean {
return element.elementType is TypeScriptClassElementType && element.children.any {
it is ES6FieldStatementImpl && it.text.contains("^static(.*)type".toRegex())
}
}

fun isActionImplExist(psiElement: PsiElement): Boolean {
return when {
isActionDispatched(psiElement) -> {
val element2 = psiElement.parent.reference?.resolve()?.navigationElement
this.findActionUsages(element2)
}

isActionClass(psiElement) -> {
this.findActionUsages(psiElement)
}

else -> false
}
}

fun getActionClassPsiElement(element: PsiElement): PsiElement? {
val endIndex = element.firstChild.nextLeafs.indexOfFirst { it.text == "{" }
return element.firstChild.nextLeafs.toList()
.subList(0, if (endIndex < 0) 0 else endIndex)
.firstOrNull { it !is PsiWhiteSpace && it.text != "class" }
}

fun findActionUsages(element: PsiElement?): Boolean {

Check notice on line 54 in src/main/kotlin/com/github/dinbtechit/ngxs/action/editor/NgxsActionUtil.kt

View workflow job for this annotation

GitHub Actions / Qodana Community for JVM

Class member can have 'private' visibility

Function 'findActionUsages' could be private

if (element == null) return false

val refs = ReferencesSearch.search(element).findAll()
for (ref in refs.toList()) {
val actionDecoratorElement = PsiTreeUtil.findFirstParent(ref.element) { it is ES6Decorator }
val hasActionDecorator = actionDecoratorElement != null
if (hasActionDecorator
&& ref.element.containingFile.name.contains(".state.ts")
) {
return true
}
}
return false
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package com.github.dinbtechit.ngxs.action.editor

import com.intellij.lang.javascript.psi.ecma6.impl.TypeScriptFunctionImpl
import com.intellij.lang.javascript.psi.ecmal4.JSAttributeList
import com.intellij.lang.javascript.types.TypeScriptClassElementType
import com.intellij.openapi.command.WriteCommandAction
import com.intellij.openapi.editor.Document
import com.intellij.openapi.fileEditor.FileDocumentManager
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.TextRange
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.PsiDocumentManager
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiManager
import com.intellij.psi.codeStyle.CodeStyleManager
import com.intellij.psi.util.elementType
import com.intellij.refactoring.suggested.endOffset
import java.util.*

class NgxsStatePsiFile(
private val ngxsStatePsiFile: VirtualFile,
val project: Project
) {

fun getTypeFromStateAnnotation(): String? {

Check notice on line 25 in src/main/kotlin/com/github/dinbtechit/ngxs/action/editor/NgxsStatePsiFile.kt

View workflow job for this annotation

GitHub Actions / Qodana Community for JVM

Class member can have 'private' visibility

Function 'getTypeFromStateAnnotation' could be private
val stateClassPsi = getStateClassElement()?.children?.firstOrNull()
if (stateClassPsi !is JSAttributeList) return null
val regex = Regex("<(.*?)>")
val matchResult = regex.find(stateClassPsi.text)
return matchResult?.groups?.get(1)?.value
}

fun getStateClassElement(): PsiElement? {

Check notice on line 33 in src/main/kotlin/com/github/dinbtechit/ngxs/action/editor/NgxsStatePsiFile.kt

View workflow job for this annotation

GitHub Actions / Qodana Community for JVM

Class member can have 'private' visibility

Function 'getStateClassElement' could be private
return PsiManager.getInstance(project).findFile(ngxsStatePsiFile)
?.children?.firstOrNull {
it.elementType is TypeScriptClassElementType
&& it.children[0] is JSAttributeList
&& it.text.contains("@State")
}
}

fun createActionMethod(actionPsiElement: PsiElement): PsiElement? {
val stateClassPsi = getStateClassElement()
if (stateClassPsi != null) {
if (stateClassPsi.node.lastChildNode.text == "}") {
val lastFunction = stateClassPsi.children.lastOrNull { it is TypeScriptFunctionImpl }
if (lastFunction != null) {
val elementText = """
@Action(${actionPsiElement.text})
${actionPsiElement.text.toCamelCase()}(ctx: StateContext<${getTypeFromStateAnnotation()}>) {
// TODO implement action
}
""".trimIndent()

val document: Document = FileDocumentManager.getInstance().getDocument(ngxsStatePsiFile) ?: return null

WriteCommandAction.runWriteCommandAction(project) {
// Check where to insert the new code
val insertOffset: Int = lastFunction.endOffset
// Insert the new code
document.insertString(insertOffset, "\n${elementText}")
PsiDocumentManager.getInstance(project).commitDocument(document)
PsiManager.getInstance(project).findFile(ngxsStatePsiFile)?.let { psiFile ->
val length = psiFile.textLength
val range = TextRange.from(insertOffset, length - insertOffset)

CodeStyleManager.getInstance(project)
.reformatText(psiFile, range.startOffset, range.endOffset)
}
}
FileDocumentManager.getInstance().saveDocument(document)
return stateClassPsi.children.lastOrNull { it is TypeScriptFunctionImpl }
}
}
}
return null
}

fun String.toCamelCase(): String = split(" ").joinToString("") { it.replaceFirstChar {

Check notice on line 79 in src/main/kotlin/com/github/dinbtechit/ngxs/action/editor/NgxsStatePsiFile.kt

View workflow job for this annotation

GitHub Actions / Qodana Community for JVM

Class member can have 'private' visibility

Function 'toCamelCase' could be private
if (it.isLowerCase()) it.titlecase(

Check notice on line 80 in src/main/kotlin/com/github/dinbtechit/ngxs/action/editor/NgxsStatePsiFile.kt

View workflow job for this annotation

GitHub Actions / Qodana Community for JVM

Nested lambda has shadowed implicit parameter

Implicit parameter 'it' of enclosing lambda is shadowed

Check notice on line 80 in src/main/kotlin/com/github/dinbtechit/ngxs/action/editor/NgxsStatePsiFile.kt

View workflow job for this annotation

GitHub Actions / Qodana Community for JVM

Nested lambda has shadowed implicit parameter

Implicit parameter 'it' of enclosing lambda is shadowed
Locale.getDefault()
) else it.toString()

Check notice on line 82 in src/main/kotlin/com/github/dinbtechit/ngxs/action/editor/NgxsStatePsiFile.kt

View workflow job for this annotation

GitHub Actions / Qodana Community for JVM

Nested lambda has shadowed implicit parameter

Implicit parameter 'it' of enclosing lambda is shadowed
} }.replaceFirstChar { it.lowercase(Locale.getDefault()) }

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package com.github.dinbtechit.ngxs.action.editor.codeIntellisense

import com.github.dinbtechit.ngxs.action.editor.NgxsActionUtil
import com.intellij.codeInspection.ProblemHighlightType
import com.intellij.lang.annotation.AnnotationHolder
import com.intellij.lang.annotation.Annotator
import com.intellij.lang.annotation.HighlightSeverity
import com.intellij.lang.javascript.psi.ecmal4.JSAttributeList
import com.intellij.lang.javascript.types.TypeScriptClassElementType
import com.intellij.openapi.util.TextRange
import com.intellij.openapi.vfs.LocalFileSystem
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiManager
import com.intellij.psi.util.elementType
import com.intellij.refactoring.suggested.endOffset
import com.intellij.refactoring.suggested.startOffset


class NgxsAnnotator : Annotator {

override fun annotate(element: PsiElement, holder: AnnotationHolder) {
var isImplementationExist = true
var problemType = ProblemHighlightType.GENERIC_ERROR_OR_WARNING
var range = TextRange(element.textRange.startOffset, element.textRange.endOffset)
var actionPsiClass: PsiElement? = null
var actionName: String? = null
var actionFileName: String? = null
var actionVirtualFile: VirtualFile? = null

if (NgxsActionUtil.isActionClass(element)) {
isImplementationExist = NgxsActionUtil.isActionImplExist(element)
problemType = ProblemHighlightType.LIKE_UNUSED_SYMBOL
try {
val classNamePsiElement = NgxsActionUtil.getActionClassPsiElement(element)
if (classNamePsiElement != null) {
range = TextRange(classNamePsiElement.startOffset, classNamePsiElement.endOffset)
actionPsiClass = classNamePsiElement
actionName = classNamePsiElement.text
actionFileName = classNamePsiElement.containingFile.name
actionVirtualFile = classNamePsiElement.containingFile.containingDirectory.virtualFile
}
} catch (e: Exception) {
throw Exception("NgxsAnnotator - Unable to establish range for the ActionClass - ${element.text}")
}

} else if (NgxsActionUtil.isActionDispatched(element)) {
isImplementationExist = NgxsActionUtil.isActionImplExist(element)
val refElement = element.parent.reference?.resolve()
if (refElement != null) {
val classNamePsiElement = NgxsActionUtil.getActionClassPsiElement(refElement)
if (classNamePsiElement != null) {
actionName = classNamePsiElement.text
actionPsiClass = classNamePsiElement
actionFileName = refElement.containingFile.name
actionVirtualFile = classNamePsiElement.containingFile.containingDirectory.virtualFile
}
}
}

if (!isImplementationExist ) {
val stateFileName = if (actionFileName != null)
"${actionFileName.split(".")[0]}.state.ts"
else "*.state.ts"
val stateFile = LocalFileSystem.getInstance().findFileByPath("${actionVirtualFile?.path}/$stateFileName")
if (stateFile != null) {
val stateClassPsi = PsiManager.getInstance(element.project).findFile(stateFile)?.children?.firstOrNull {
it.elementType is TypeScriptClassElementType
&& it.children[0] is JSAttributeList
&& it.text.contains("@State")
}
if (stateClassPsi != null) {
if (stateClassPsi.node.lastChildNode.text == "}") {
stateClassPsi.node.lastChildNode
}

}
}

if (actionName == null) actionName = element.text
holder.newAnnotation(HighlightSeverity.WARNING, "@Action(${actionName}) not found in $stateFileName")
.range(range)
.highlightType(problemType)
.withFix(
NgxsCreateActionQuickFix("Create @Action(${actionName}) in $stateFileName.",
actionPsiClass!!, stateFile))
.create()
}

}
}

Loading

0 comments on commit 5faef59

Please sign in to comment.