为WebStorm构建插件- JavaScript开发者教程,第2部分
在第1部分在本系列中,我们讨论了插件结构和插件模板,并演示了如何在不编写任何代码的情况下构建一个简单的插件。在这篇博文中,我们将讨论插件的另一个非常重要的方面:动作。
的简单版本eCSStractor插件,它将获取HTML内容并从中生成带有类名的CSS代码。
对于下面的HTML:
test
我们的插件将生成结果CSS文件:
.hello1 {} .hello2 {}
请注意,这将不可能自定义我们的新插件。最初的eCSStractor插件支持BEM、LESS文件和许多其他功能,但我们决定去掉这些功能,使示例尽可能简单。
步骤0:前提条件
我们在本教程的这一部分中构建的插件比第1部分中的插件要复杂得多,为了构建它,我们需要使用Kotlin。在开始之前,请先看一看需求为WebStorm构建基于kotlin的插件。要完成本教程,您需要了解Java/Kotlin基础知识:包、导入、类/接口和对象,以及熟悉swing相关的概念,如事件调度线程.
如果您不熟悉Kotlin,您可以使用免费版开始学习它Kotlin基本轨道来自JetBrains Academy。
步骤1:为开发准备插件
我们需要创建一个新的插件项目,并在IntelliJ IDEA中设置一个基本的插件构建。的前7个步骤实际上已经涵盖了这一部分上一篇博文.一旦你有了插件模板,配置Java,自定义插件构建,更新你的plugin . xml文件,您可以进行下一步。
请注意,由于2021.3版本非常接近,我们可以将插件的兼容性扩展到新版本gradle.properties:
* . plugininincebuild = 212 pluginUntilBuild = 213
步骤2:指定依赖项
我们的新插件将与CSS一起工作,所以我们需要指定CSS插件为依赖.
要做到这一点,我们需要在我们的plugin . xml文件:
< >取决于com.intellij.css < /取决于> < >取决于com.intellij.modules.xml < / >取决于
我们还需要将此代码添加到gradle.properties:
platformPlugins = com.intellij.css
类中不需要添加HTML/XML依赖项gradle.properties文件,因为它已经是平台的一部分,但是我们仍然需要在plugin . xml文件,以表明我们计划使用该功能。
步骤3:为该操作添加一个新的Kotlin类
我们计划实施一个新的自定义动作提供必要的功能。
首先,我们需要为源文件创建相应的目录结构。源文件应该存储在src /主要目录中。例如,由于我们可以在IntelliJ IDEA中使用任何JVM语言,因此源代码通常按语言进行分割java或芬兰湾的科特林.
我们想要为这个插件使用Kotlin,所以让我们创建一个芬兰湾的科特林目录下src /主要.类型下创建顶级包芬兰湾的科特林目录,使用上下文菜单。右键单击该目录并选择新|包.我们可以用任何名字,在这种情况下extract.css是个不错的选择。让我们还创建一个名为行动内部extract.css让我们存储的类有更多的结构。
![extractCSSaction](http://www.glenndubin.com/wp-content/uploads/2021/11/extractCSSaction.png)
创建ExtractCSSAction.kt文件内容如下:
openapi. actionsystem .AnActionEvent import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.ui.Messages类ExtractCSSAction: AnAction() {override fun actionPerformed(e: AnActionEvent) {Messages.showMessageDialog(e: AnActionEvent)project, "Hello, extract css", "Welcome", null)}}
我们不打算仔细研究实现,因为我们的主要目标是为测试做出一个动作。后面将介绍真正的实现。
步骤4:注册操作
创建操作类后,我们需要将其注册到plugin . xml文件。在资源/ meta - inf / plugin . xml文件,在顶层中添加动作注册< idea-plugin >
标签:
.
在这个样本中,文本
行动的名称和类
将用于操作实现的完整类名。
步骤5:运行操作
让我们运行我们的插件,并确保动作能够工作。我们在上一篇文章中提供了运行和调试插件的说明,请参阅中的“运行和调试”部分步骤9:
我们只需要点击运行或调试对应按钮运行插件导航栏上的配置。
请记住,当您运行或调试插件时,您将有两个开放的ide:第一个用于开发插件,第二个用于测试插件。
在启动测试IDE之后,我们必须打开一个项目或创建一个项目,并运行我们刚刚创建的操作。该操作没有分配给它的快捷方式,也没有添加到任何菜单中。这意味着运行它的唯一方法是输入动作名称,提取CSS,进入找到行动(⇧⌘A / Ctrl+Shift+A)或到处寻找(Shift+Shift).
![执行这一指令](http://www.glenndubin.com/wp-content/uploads/2021/11/run-the-action.png)
运行该操作后,您将看到下面的注释Messages.showMessageDialog ()
.
![hello-action](http://www.glenndubin.com/wp-content/uploads/2021/11/hello-action.png)
步骤6:处理HTML文件
现在,我们已经准备好实现一个“真正的”操作,而不仅仅是测试操作。但我们先去掉这条线Messages.showMessageDialog
从actionPerformed ()
.
你可能猜到了actionPerformed ()
包含操作的实现。这就是我们需要编写所有必需代码的地方。
在算法上,我们需要:
- 获取上下文文件并确保它是HTML。
- 收集带有该名称的所有属性类从文件中。
- 创建一个新的CSS文件,其中包含属性中定义的类名。
让我们从头开始。的actionPerformed ()
方法只接收一个参数艾凡:AnActionEvent
,context文件应该在对象内部。
AST和PSI
IDE通常不处理常规的文件内容,而是对文件进行抽象。因此,我们要用aψ(程序结构接口),它是对AST(抽象语法树)的抽象。PSI与AST类似,并以某种方式表示它,因为它们具有相同的功能getParent ()
,调用getChildren ()
方法,但它还提供了一种更简单的处理树的方法。
作为一个例子,让我们考虑一个带有子标签的标签:
div1div2
如果希望使用AST获得所有嵌套标记,则需要迭代子标记并找到其中的所有标记。另一方面,我们有PSI接口XmlTag
,它有# getSubTags ()
方法。当然,底层的方法仍然使用AST来获取子标记,但是从客户端来看,这是一种使用树的更方便的方式。
首先,我们想从动作的上下文中得到文件的对应PSI。
我们可以用下面的代码做到这一点:
val xmlFile = e.getData(CommonDataKeys.PSI_FILE) as?:返回
IntelliJ平台没有单独的HTML文件接口。所有的PSI HTML文件实现XmlFile
接口,但是也可以使该操作对XML文件可用。
项目
我们想要在一个开放的项目中处理动作,所以我们需要确保相应的对象存在:
Val project = e.project ?:返回
这非常有用,因为我们需要将项目传递给其他几个方法。
参观
现在我们已经提取了文件,所以接下来需要做的是找到所有具有该名称的属性类.
当然,我们可以手动处理文件结构:获取文件,获取所有子标记,处理它们的属性,然后处理子标记的子标记的任何属性,等等。但是,由于我们必须在IDE中一直进行这种遍历,因此实际上可以使用IDE中的类来简化这个过程。对于XML文件,可以使用XmlRecursiveElementWalkingVisitor
已经实现递归访问的类。有一个游客
类可以用于IDE中支持的每种语言,例如。JavaRecursiveElementWalkingVisitor
,JSRecursiveWalkingElementVisitor
等。
我们需要重写visitXmlAttribute ()
方法,并将访问者传递给acceptChildren
的方法xmlFile
:
xmlFile。acceptChildren(对象:XmlRecursiveElementWalkingVisitor() {override fun visitXmlAttribute(属性:XmlAttribute) {}})
这段代码已经访问了文件中的所有属性。接下来,我们需要确保当前属性具有名称类
,我们需要将属性的值保存到某个地方。
val classNames = mutableSetOf() xmlFile. valacceptChildren(object : XmlRecursiveElementVisitor() { override fun visitXmlAttribute(attribute: XmlAttribute?) { if (attribute.name == "class") { classNames.addAll(attribute.value?.split(" ")?.filter(String::isNotBlank) ?: emptyList()) } } })
就是这样!我们已经收集了所有需要处理的CSS类名,所以下一步是创建一个新的CSS文件。
步骤7:生成CSS文件
首先,我们需要创建最终的文件内容:
val newContent = classes.joinToString("\n"){"。$it {}"}
接下来,我们需要用这些内容创建一个新的CSS文件。但在此之前,让我们讨论一些重要的事情。
为了开发可以在IDE中使用模型的插件,我们必须了解几个概念。
读写锁
通常,IntelliJ平台中与代码相关的数据结构由一个单一的读/写锁.
每当我们需要从模型中读取数据时,就需要使用读锁。对AST或PSI的任何访问都应该使用读锁封装。
每当我们需要在模型中写入时,例如创建一个新文件或通过重构修改AST,都需要写入锁。写锁必须从EDT(事件调度线程)调用。注意,从EDT调用的代码在默认情况下总是具有读锁,而不显式地获取它。
IntelliJ平台有许多扩展点,您可以使用这些扩展点定制IDE的行为,其中许多扩展点已经在读取操作中调用。您需要从文档或使用调试器手动检查每个扩展点的读锁。
幸运的是,在我们的例子中,已经获得了读锁。此外,actionPerformed ()
方法在默认情况下在EDT上被调用,因此我们可以在不将代码移动到另一个线程的情况下获得写锁。
命令中提供更详细的信息时,写操作中的所有操作都必须用一些命令进行包装当地的历史而且撤销/重做行动。
下一步是启动写操作并调用创建所需CSS文件的命令。
我们可以用WriteCommandAction
类,它提供了一种同时定义命令打电话给写同时行动:
WriteCommandAction .writeCommandAction(project) .withName("正在创建CSS文件").run<异常> {}
中创建CSS文件的代码运行{}
的身体。为了简单起见,让我们在相同的目录中创建一个与当前HTML文件同名的新CSS文件。
WriteCommandAction . WriteCommandAction(项目). withname("创建CSS文件").run{val newName = "${FileUtil.getNameWithoutExtension(xmlFile.name)}. CSS " val newFile = PsiFileFactory.getInstance(项目). createfilefromtext (newName, CssFileType. txt)。实例,newContent) val目录= xmlFile。: return@run directory.add(newFile)}
逻辑已经完成。现在让我们运行插件,看看它是如何工作的。
- 运行插件。
- 创建一个新的HTML文件,内容如下:test.
- 运行我们的提取CSS操作使用找到行动或到处寻找.
- 在同一目录中检查创建的CSS文件和HTML文件。
如果你正确地做了所有的事情,CSS文件将有以下内容:
.hello1 {} .hello2 {}
8 .化妆品(可选)
我们的插件工作,但有几件事可以改进。
打开新文件
如果能够在操作之后打开创建的文件就太好了。我们可以用导航()
方法来执行此操作。请注意添加(newFile中)
方法将创建一个新的CSS文件实例,而不是当前的实例,因此我们需要调用导航()
的结果add ()
操作而不是newFile中
对象:
(directory.add (newFile中)?XmlFile) ? .navigate(真正的)
重新格式化新文件
创建新文件后,如果能够根据项目的CSS代码样式重新格式化它们,那就太好了:
val directory = xmlFile。: 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文件。在操作注册内部,我们必须添加子标记<快捷键>
具有相应的选项:
的keymap
属性,它指定键映射。
![assign-shortcut](http://www.glenndubin.com/wp-content/uploads/2021/11/assign-shortcut.png)
您可以为不同的键映射分配不同的快捷方式。的值美元的违约
对应于默认键映射,默认键映射是所有其他键映射的基础。
将操作添加到上下文菜单中
在某些情况下,从上下文菜单运行操作要比使用快捷方式容易得多。要做到这一点,只需添加一个额外的嵌套增加了群组
标签。行动
标签。
这个名字EditorPopupMenu
标识我们想要显示操作的位置:编辑器上下文菜单。中可以指定许多可能的变体组id
,并且几乎所有的都可以通过代码补全获得,并且有自描述的名称。
![添加操作](http://www.glenndubin.com/wp-content/uploads/2021/11/add-action.png)
减少动作的上下文
该操作可用于所有文件,但仅适用于HTML(XML)。这意味着我们可以限制操作的可用性。
的update ()
方法某个活动
类用于控制行为。方法中的方法ExtractCSSAction
类,并根据当前上下文设置可见性信息:
重写有趣的更新(e: AnActionEvent) {e.c iation . isenabledandvisible = e.getData(commondatakeis . psi_file)是XmlFile && e.c eim eproject != null}
进一步的改善
插件的第一个版本非常简单,仍然有很多方法可以改进它:
- 提供一个将类提取到剪贴板的选项。
- 提供一个以BEM格式生成输出的选项,就像在原始插件中那样。
- 提供一个选项来生成LESS / SASS代码,而不是普通的CSS。
- 提供一个选项来为选定的代码片段运行操作。
- 支持提取id。
- 支持JSX。
因为有一个请求从社区来看,我们的团队目前正在努力改进插件,我们计划实现上面提到的所有功能。你可以随时查看插件的最新版本的源代码在这里.
这就是本系列的第二部分。我们希望这一部分能够帮助您理解与操作和面向语言的插件相关的一些基本概念。在下一期文章中,我们计划为真正的JS框架支持编写一些更复杂的东西。请继续关注!
如果您有任何问题或意见,请在下方留言或与我们联系推特.您也可以通过我们的专用服务与我们联系松弛的通道对于插件开发者。
WebStorm团队