自定义右键菜单,实现一个提取CSS的效果
我们的新插件将与CSS配合使用,因此我们需要将CSS插件指定为依赖项。
<depends>com.intellij.css</depends>
为此,我们需要在plugin.xml文件中包含以下代码:
intellij {
version.set("2021.2")
type.set("IU") // Target IDE Platform
plugins.set(listOf(/* Plugin Dependencies */"JavaScriptLanguage", "CSS"))
}
首先,我们需要为源文件创建相应的目录结构。源文件应存储在src/main目录下。由于我们可以在IntelliJ IDEA中使用任何JVM语言,因此源通常按语言划分,例如java或kotlin。

创建具有以下内容的ExtractCSSAction.kt文件:
package com.example.docstest1
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.ui.Messages
class ExtractCSSAction : AnAction() {
override fun actionPerformed(e: AnActionEvent) {
Messages.showMessageDialog(e.project, "Hello, extract css", "Welcome", null)
}
}
创建操作类后,我们需要将其注册到plugin.xml文件中。在theresources/META-INF/plugin.xml文件中,在顶级 标签中添加操作注册:
<idea-plugin>
<actions>
<action text="Extract CSS" class="com.example.docstest1.ExtractCSSAction" />
</actions>
</idea-plugin>
让我们运行我们的插件,并确保操作有效。我们在上一篇文章中提供了运行和调试插件的说明
启动测试IDE后,我们必须打开一个项目或创建一个项目,并运行我们刚刚创建的操作。该操作没有分配给它的快捷方式,也没有添加到任何菜单中。这意味着运行它的唯一方法是将操作名称Extract CSS输入Find Action(⇧⌘A / Ctrl+Shift+A)或Search Everywhere(⇧⇧ / Shift+Shift)。


我们需要:
IDE通常不使用常规文件内容,而是对文件进行抽象操作。因此,我们将使用PSI(程序结构接口),这是AST(抽象语法树)上的抽象。PSI与AST相似,并以某种方式表示它,因为它具有相同的getParent()、getChildren()方法,但它也提供了一种更简单的树处理方式。
因此,首先,我们希望从操作的上下文中获取文件的相应PSI。我们可以使用以下代码来做到这一点:
val xmlFile = e.getData(CommonDataKeys.PSI_FILE) as? XmlFile ?: return
所有PSI HTML文件都实现了XmlFile接口
我们希望在开放项目中处理该操作,因此我们需要确保相应的对象存在:
val project = e.project ?: return
我们现在已经提取了文件,所以我们接下来需要做的就是找到带有名称类的所有属性。
当然,我们可以手动处理文件结构:取文件,获取所有子标签,处理其属性,然后处理子标签子标签的任何属性,等等。但是,由于我们必须在IDE中一直进行这种遍历,因此实际上我们可以使用其中的类来简化流程。对于XML文件,我们可以使用已经实现递归访问的XmlRecursiveElementWalkingVisitor类。IDE中每种受支持的语言都有visitor类,例如JavaRecursiveElementWalkingVisitor、JSRecursiveWalkingElementVisitor等。
我们需要覆盖visitXmlAttribute()方法,并将访问者传递给xmlFile的acceptChildren方法:
xmlFile.acceptChildren(object : XmlRecursiveElementWalkingVisitor() {
override fun visitXmlAttribute(attribute: XmlAttribute) {
}
})
此代码已经访问了文件中的所有属性。接下来,我们需要确保当前属性具有名称class,并且我们需要将属性的值保存在某个地方。
val classNames = mutableSetOf<String>()
xmlFile.acceptChildren(object : XmlRecursiveElementVisitor() {
override fun visitXmlAttribute(attribute: XmlAttribute?) {
if (attribute.name == "class") {
classNames.addAll(attribute.value?.split(" ")?.filter(String::isNotBlank) ?: emptyList())
}
}
})
就这样!我们已经收集了所有需要处理的CSS类名,因此我们的下一步是创建一个新的CSS文件。
下一步是启动写入操作,然后调用创建所需CSS文件的命令。
我们可以使用WriteCommandAction构建器类,该类提供了一种同时定义命令和调用write操作的方法。
我们需要编写代码,在run {}正文中创建一个CSS文件。为了简单起见,让我们在同一目录中创建一个与当前HTML文件名称相同的新CSS文件。
WriteCommandAction
.writeCommandAction(project)
.withName("Create CSS File")
.run<Exception> {
val newName = "${FileUtil.getNameWithoutExtension(xmlFile.name)}.css"
val newFile = PsiFileFactory.getInstance(project)
.createFileFromText(newName, CssFileType.INSTANCE, newContent)
val directory = xmlFile.parent ?: return@run
directory.add(newFile)
}
逻辑就完成了。现在,让我们运行这个插件,看看它是如何工作的。
如果您已正确完成所有操作,CSS文件将包含以下内容:
.hello1 {}
.hello2 {}
如果能够在操作后打开创建的文件,那就太好了。我们可以使用添加文件的navigate()方法来做到这一点。请注意,add(newFile)方法将创建一个新的CSS文件实例,而不是当前文件实例,因此我们需要调用navigate()作为theaddadd()操作的结果,而不是newFile对象:
(directory.add(newFile) as? XmlFile)?.navigate(true)
创建新文件后,能够根据项目的CSS代码样式重新格式化它们也很好:
val directory = xmlFile.parent ?: return@run
val actualFile = directory.add(newFile) as? PsiFile ?: return@run
CodeStyleManager.getInstance(project).reformat(actualFile)
actualFile.navigate(true)
通过我们最初的实现,新操作只能从搜索中获得,这并不特别方便。让我们通过为操作分配一个快捷方式来解决这个问题。我们可以使用原始eCSStractor插件中使用的快捷方式:⌘⇧X / Ctrl+Shift+X。要为操作分配快捷方式,我们需要编辑plugin.xml文件。在操作注册中,我们必须添加带有相应选项的子标签 :
<action text="Extract CSS" class="extract.css.actions.ExtractCSSAction">
<keyboard-shortcut first-keystroke="control shift X" keymap="$default"/>
</action>
在某些情况下,从上下文菜单运行操作比使用快捷方式要容易得多。为此,只需在action标签内添加一个额外的嵌套add-to-group标签。
<action text="Extract CSS" class="extract.css.actions.ExtractCSSAction">
<keyboard-shortcut first-keystroke="control shift X" keymap="$default" />
<add-to-group group-id="EditorPopupMenu" />
</action>
EditorPopupMenu的名称标识了我们要显示操作的位置:编辑器上下文菜单。许多可能的变体都可以在group-id中指定,几乎所有变体都可以通过代码完成获得,并具有自我描述的名称。

该操作适用于所有文件,但仅适用于HTML(XML)。这意味着我们可以限制命令的可用性。
AnAction类的update()方法用于控制行为。我们需要覆盖ExtractCSSAction类中的方法,并根据当前上下文设置可见性信息:
override fun update(e: AnActionEvent) {
e.presentation.isEnabledAndVisible =
e.getData(CommonDataKeys.PSI_FILE) is XmlFile && e.project != null
}