Skip to content

Commit ed9245f

Browse files
author
kzheart
committed
feat(lang): 添加语言键验证和使用情况分析功能
1 parent 24a61e7 commit ed9245f

File tree

9 files changed

+796
-1
lines changed

9 files changed

+796
-1
lines changed

src/main/kotlin/org/tabooproject/development/inlay/LangColorSettingsPage.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,11 @@ class LangColorSettingsPage : ColorSettingsPage {
6565
}
6666
}
6767

68+
// 添加语言键验证相关的颜色设置
69+
attributes.add(AttributesDescriptor("TabooLib Lang: Missing Key", LangKeyValidationAnnotator.MISSING_LANG_KEY_ATTRIBUTES))
70+
attributes.add(AttributesDescriptor("TabooLib Lang: Valid Key", LangKeyValidationAnnotator.VALID_LANG_KEY_ATTRIBUTES))
71+
attributes.add(AttributesDescriptor("TabooLib Lang: Unused Key", LangFileUnusedAnnotator.UNUSED_LANG_KEY_ATTRIBUTES))
72+
6873
return attributes.toTypedArray()
6974
}
7075

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package org.tabooproject.development.inlay
2+
3+
import com.intellij.lang.annotation.AnnotationHolder
4+
import com.intellij.lang.annotation.Annotator
5+
import com.intellij.lang.annotation.HighlightSeverity
6+
import com.intellij.openapi.editor.colors.EditorColorsManager
7+
import com.intellij.openapi.editor.colors.TextAttributesKey
8+
import com.intellij.openapi.editor.markup.TextAttributes
9+
import com.intellij.psi.PsiElement
10+
import org.yaml.snakeyaml.Yaml
11+
import java.awt.Color
12+
import java.awt.Font
13+
14+
/**
15+
* TabooLib 语言文件未使用键注解器
16+
*
17+
* 为语言文件中未使用的键添加灰色显示
18+
*
19+
* @since 1.42
20+
*/
21+
class LangFileUnusedAnnotator : Annotator {
22+
23+
companion object {
24+
// 定义未使用语言键的高亮样式
25+
val UNUSED_LANG_KEY_ATTRIBUTES = TextAttributesKey.createTextAttributesKey(
26+
"TABOOLIB_UNUSED_LANG_KEY",
27+
TextAttributes().apply {
28+
foregroundColor = Color.GRAY
29+
fontType = Font.ITALIC
30+
}
31+
)
32+
}
33+
34+
override fun annotate(element: PsiElement, holder: AnnotationHolder) {
35+
val file = element.containingFile?.virtualFile ?: return
36+
37+
// 只处理语言文件
38+
if (!LangFiles.isLangFile(file)) return
39+
40+
// 检查是否是YAML键值对的键部分
41+
val langKey = extractLangKeyFromElement(element) ?: return
42+
43+
// 检查这个语言键是否被使用
44+
val project = element.project
45+
val isUsed = LangUsageAnalyzer.isLangKeyUsed(project, langKey)
46+
47+
if (!isUsed) {
48+
// 为未使用的语言键添加灰色高亮
49+
holder.newSilentAnnotation(HighlightSeverity.INFORMATION)
50+
.range(element.textRange)
51+
.textAttributes(UNUSED_LANG_KEY_ATTRIBUTES)
52+
.tooltip("语言键 '$langKey' 未在项目中使用")
53+
.create()
54+
}
55+
}
56+
57+
/**
58+
* 从PSI元素中提取语言键
59+
* 这是一个简化的实现,实际需要根据YAML文件结构来解析
60+
*/
61+
private fun extractLangKeyFromElement(element: PsiElement): String? {
62+
val text = element.text.trim()
63+
64+
// 简单的YAML键值对匹配
65+
if (text.contains(":")) {
66+
val key = text.substringBefore(":").trim()
67+
if (key.isNotEmpty() && !key.startsWith("#")) {
68+
return key
69+
}
70+
}
71+
72+
return null
73+
}
74+
}
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
package org.tabooproject.development.inlay
2+
3+
import com.intellij.codeInsight.daemon.RelatedItemLineMarkerInfo
4+
import com.intellij.codeInsight.daemon.RelatedItemLineMarkerProvider
5+
import com.intellij.codeInsight.navigation.NavigationGutterIconBuilder
6+
import com.intellij.icons.AllIcons
7+
import com.intellij.openapi.editor.markup.GutterIconRenderer
8+
import com.intellij.psi.PsiElement
9+
import com.intellij.psi.PsiManager
10+
import org.jetbrains.kotlin.psi.KtCallExpression
11+
import org.jetbrains.kotlin.psi.KtStringTemplateExpression
12+
import org.tabooproject.development.isSendLangCall
13+
import javax.swing.Icon
14+
15+
/**
16+
* TabooLib 语言文件使用情况行标记提供器
17+
*
18+
* 在语言文件中显示每个语言键的使用次数,并提供跳转到使用位置的功能
19+
*
20+
* @since 1.42
21+
*/
22+
class LangFileUsageLineMarker : RelatedItemLineMarkerProvider() {
23+
24+
override fun collectNavigationMarkers(
25+
element: PsiElement,
26+
result: MutableCollection<in RelatedItemLineMarkerInfo<*>>
27+
) {
28+
val file = element.containingFile?.virtualFile ?: return
29+
30+
// 只处理语言文件
31+
if (!LangFiles.isLangFile(file)) return
32+
33+
// 提取语言键
34+
val langKey = extractLangKeyFromElement(element) ?: return
35+
36+
// 查找使用位置
37+
val project = element.project
38+
val usages = LangUsageAnalyzer.findUsages(project, langKey)
39+
40+
if (usages.isEmpty()) {
41+
// 未使用的键,显示警告图标
42+
val builder = NavigationGutterIconBuilder
43+
.create(AllIcons.General.InspectionsEye)
44+
.setTooltipText("语言键 '$langKey' 未被使用")
45+
.setPopupTitle("未使用的语言键")
46+
.setEmptyPopupText("此语言键在项目中未被使用")
47+
48+
result.add(builder.createLineMarkerInfo(element))
49+
} else {
50+
// 有使用的键,显示使用次数和跳转选项
51+
val psiManager = PsiManager.getInstance(project)
52+
val targets = mutableListOf<PsiElement>()
53+
54+
usages.forEach { usage ->
55+
val psiFile = psiManager.findFile(usage.file)
56+
if (psiFile != null) {
57+
val targetElement = findElementAtOffset(psiFile, usage.offset)
58+
if (targetElement != null) {
59+
targets.add(targetElement)
60+
}
61+
}
62+
}
63+
64+
if (targets.isNotEmpty()) {
65+
val usageCount = usages.size
66+
val tooltip = "语言键 '$langKey' 被使用了 $usageCount"
67+
68+
val builder = NavigationGutterIconBuilder
69+
.create(getUsageIcon(usageCount))
70+
.setTargets(targets)
71+
.setTooltipText(tooltip)
72+
.setPopupTitle("语言键使用位置")
73+
.setEmptyPopupText("无法找到使用位置")
74+
75+
result.add(builder.createLineMarkerInfo(element))
76+
}
77+
}
78+
}
79+
80+
/**
81+
* 从元素中提取语言键
82+
*/
83+
private fun extractLangKeyFromElement(element: PsiElement): String? {
84+
val text = element.text.trim()
85+
86+
// 简单的YAML键值对匹配
87+
if (text.contains(":")) {
88+
val key = text.substringBefore(":").trim()
89+
if (key.isNotEmpty() && !key.startsWith("#") && !key.startsWith("\"") && !key.startsWith("'")) {
90+
return key
91+
}
92+
}
93+
94+
return null
95+
}
96+
97+
/**
98+
* 根据使用次数获取相应图标
99+
*/
100+
private fun getUsageIcon(usageCount: Int): Icon {
101+
return when {
102+
usageCount == 0 -> AllIcons.General.InspectionsEye
103+
usageCount == 1 -> AllIcons.Gutter.Unique
104+
usageCount <= 5 -> AllIcons.General.ArrowRight
105+
else -> AllIcons.General.BalloonInformation
106+
}
107+
}
108+
109+
/**
110+
* 在指定偏移位置查找PSI元素
111+
*/
112+
private fun findElementAtOffset(psiFile: com.intellij.psi.PsiFile, offset: Int): PsiElement? {
113+
val elementAtOffset = psiFile.findElementAt(offset) ?: return null
114+
115+
// 查找包含的sendLang调用
116+
val callExpression = com.intellij.psi.util.PsiTreeUtil.getParentOfType(elementAtOffset, KtCallExpression::class.java)
117+
if (callExpression != null && isSendLangCall(callExpression)) {
118+
// 返回语言键参数的字符串模板
119+
val arguments = callExpression.valueArgumentList?.arguments
120+
if (!arguments.isNullOrEmpty()) {
121+
val firstArg = arguments[0].getArgumentExpression()
122+
if (firstArg is KtStringTemplateExpression) {
123+
return firstArg
124+
}
125+
}
126+
}
127+
128+
return elementAtOffset
129+
}
130+
}

src/main/kotlin/org/tabooproject/development/inlay/LangFoldingSettings.kt

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,12 @@ class LangFoldingOptionsProvider :
3131
// 设置变更后刷新折叠
3232
LangFoldingSettingsListener.refreshAllEditors()
3333
}
34+
checkBox(
35+
"Highlight valid language keys",
36+
LangFoldingSettings.instance::showValidLangKeyHighlight,
37+
) {
38+
LangFoldingSettings.instance.showValidLangKeyHighlight = it
39+
}
3440
}
3541
}
3642

@@ -42,7 +48,8 @@ class LangFoldingSettings : PersistentStateComponent<LangFoldingSettings.State>
4248

4349
data class State(
4450
var shouldFoldTranslations: Boolean = true,
45-
var showColorCodes: Boolean = false
51+
var showColorCodes: Boolean = false,
52+
var showValidLangKeyHighlight: Boolean = false
4653
)
4754

4855
private var state = State()
@@ -68,6 +75,12 @@ class LangFoldingSettings : PersistentStateComponent<LangFoldingSettings.State>
6875
state.showColorCodes = value
6976
}
7077

78+
var showValidLangKeyHighlight: Boolean
79+
get() = state.showValidLangKeyHighlight
80+
set(value) {
81+
state.showValidLangKeyHighlight = value
82+
}
83+
7184
companion object {
7285
val instance: LangFoldingSettings
7386
get() = ApplicationManager.getApplication().getService(LangFoldingSettings::class.java)
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
package org.tabooproject.development.inlay
2+
3+
import com.intellij.openapi.util.TextRange
4+
import com.intellij.psi.*
5+
import com.intellij.psi.search.GlobalSearchScope
6+
import com.intellij.psi.util.PsiTreeUtil
7+
import com.intellij.util.ProcessingContext
8+
import org.jetbrains.kotlin.psi.KtCallExpression
9+
import org.jetbrains.kotlin.psi.KtStringTemplateExpression
10+
import org.tabooproject.development.isSendLangCall
11+
12+
/**
13+
* TabooLib 语言键引用提供器
14+
*
15+
* 提供从语言文件到代码使用位置的引用跳转
16+
*
17+
* @since 1.42
18+
*/
19+
class LangKeyReferenceProvider : PsiReferenceProvider() {
20+
21+
override fun getReferencesByElement(element: PsiElement, context: ProcessingContext): Array<PsiReference> {
22+
// 检查是否是语言文件中的键
23+
val file = element.containingFile?.virtualFile
24+
if (file == null || !LangFiles.isLangFile(file)) {
25+
return PsiReference.EMPTY_ARRAY
26+
}
27+
28+
val langKey = extractLangKeyFromYamlElement(element) ?: return PsiReference.EMPTY_ARRAY
29+
30+
return arrayOf(LangKeyReference(element, langKey))
31+
}
32+
33+
/**
34+
* 从YAML元素中提取语言键
35+
*/
36+
private fun extractLangKeyFromYamlElement(element: PsiElement): String? {
37+
val text = element.text.trim()
38+
39+
// 简单的YAML键匹配
40+
if (text.contains(":")) {
41+
val key = text.substringBefore(":").trim()
42+
if (key.isNotEmpty() && !key.startsWith("#")) {
43+
return key
44+
}
45+
}
46+
47+
return null
48+
}
49+
}
50+
51+
/**
52+
* TabooLib 语言键引用
53+
*/
54+
class LangKeyReference(
55+
element: PsiElement,
56+
private val langKey: String
57+
) : PsiReferenceBase<PsiElement>(element) {
58+
59+
override fun resolve(): PsiElement? {
60+
// 查找使用这个语言键的第一个位置
61+
val usages = LangUsageAnalyzer.findUsages(element.project, langKey)
62+
if (usages.isEmpty()) return null
63+
64+
val firstUsage = usages.first()
65+
val psiManager = PsiManager.getInstance(element.project)
66+
val psiFile = psiManager.findFile(firstUsage.file) ?: return null
67+
68+
// 找到具体的PSI元素
69+
return findElementAtOffset(psiFile, firstUsage.offset)
70+
}
71+
72+
override fun getVariants(): Array<Any> {
73+
// 返回所有可能的语言键
74+
val allLangs = LangIndex.getProjectDefaultLangs(element.project)
75+
return allLangs.map { it.key }.toTypedArray()
76+
}
77+
78+
override fun handleElementRename(newElementName: String): PsiElement {
79+
// 处理重命名(暂时不实现)
80+
return element
81+
}
82+
83+
/**
84+
* 在指定偏移位置查找PSI元素
85+
*/
86+
private fun findElementAtOffset(psiFile: PsiFile, offset: Int): PsiElement? {
87+
val elementAtOffset = psiFile.findElementAt(offset) ?: return null
88+
89+
// 查找包含的sendLang调用
90+
val callExpression = PsiTreeUtil.getParentOfType(elementAtOffset, KtCallExpression::class.java)
91+
if (callExpression != null && isSendLangCall(callExpression)) {
92+
// 返回语言键参数的字符串模板
93+
val arguments = callExpression.valueArgumentList?.arguments
94+
if (!arguments.isNullOrEmpty()) {
95+
val firstArg = arguments[0].getArgumentExpression()
96+
if (firstArg is KtStringTemplateExpression) {
97+
return firstArg
98+
}
99+
}
100+
}
101+
102+
return elementAtOffset
103+
}
104+
}
105+
106+
/**
107+
* TabooLib 语言键引用贡献器
108+
*
109+
* 为语言文件中的键注册引用提供器
110+
*/
111+
class LangKeyReferenceContributor : PsiReferenceContributor() {
112+
113+
override fun registerReferenceProviders(registrar: PsiReferenceRegistrar) {
114+
// 为YAML文件中的所有元素注册引用提供器
115+
registrar.registerReferenceProvider(
116+
com.intellij.patterns.PlatformPatterns.psiElement(),
117+
LangKeyReferenceProvider()
118+
)
119+
}
120+
}

0 commit comments

Comments
 (0)