文件模板机制允许生成包含重复文本和模式的文件和代码片段。它的主要目的是通过自动生成样板代码来减轻用户不必要的手动工作。件模板可用于创建新的项目文件,其中填充了预定义的内容,例如特定于特定文件类型和上下文的代码脚手架或许可证标头。
模板基于Apache Velocity实现
一、概述
模板文件存放在本地的根目录,如果没有自定义的模板文件,则没有此目录。目前idea不支持自定义的插件中同时创建带有多个文件的模板。
~/Library/Application Support/JetBrains/IntelliJIdea2023.1/fileTemplates
创建文件模板有几下几种方式:1、File | Save File as Template…;2、IntelliJ IDEA | Settings | Editor | File and Code Templates。文件模板可分为几下几类:
- File:文件类别包含用于创建新文件的模板,会出现在File | New File菜单中的内容。这是主要类别,它包括直接放在fileTemplates目录中的所有模板;
- include:包含可重复使用的片段,使用 Apache Velocity 指令的
#parse()
- 将其包含在其他文件模板中,例如,许可标头或文档注释框架。存放在fileTemplates/includes目录中;
- code:包含用于插入现有文件的模板,存放在fileTemplates/code目录中;
- internal:包含默认情况下在 IDE 设置中不可见且用户无法编辑的模板,存放在fileTemplates/internal目录中
二、使用插件创建自定义的文件模板
注意必须为模板文件添加.ft扩展名,例如My Class.java必须重命名为My Class.java.ft。IDE 设置中显示的模板名称和扩展名将自动从文件名中提取。
1、创建说明文档
模板文件名:My Class.java.ft,则说明文件名:My Class.java.html。
提供默认文件模板属性
正常情况下idea已预置了很多变量,如果需要自定义变量可通过扩展com.intellij.defaultTemplatePropertiesProvider来实现。
public class TemplatePackagePropertyProvider implements DefaultTemplatePropertiesProvider {
@Override
public void fillProperties(@NotNull final PsiDirectory directory, @NotNull final Properties props) {
JavaTemplateUtil.setPackageNameAttribute(props, directory);
}
}
Idea预置的变量
多变的 | 描述 |
| 当前系统日期 |
| 该月的当前日期 |
| 从内容根目录到新文件目录的路径 |
| 美元符号 |
| 新文件的名称 |
| 当前时间 |
| 当前分钟 |
| 当前秒 |
| 这个月 |
| 当前月份的全名(一月、二月等) |
| 当前月份名称的前三个字母(Jan、Feb 等) |
| 新实体的名称(文件、%class%、接口等) |
| 创建新类或接口文件的目标包的名称 |
| IDE 的名称(例如,IntelliJ IDEA) |
| 当前项目名称 |
| 当前系统时间 |
| 当前用户的登录名 |
| 今年 |
3、相关API及示例代码
文件模板主要由FileTemplateManager类来管理,比如获取code类别的模板,则可以使用以下代码:
FileTemplate template = FileTemplateManager.getInstance(project)
.getCodeTemplate("Test Class.java");
//得到模板内容
Properties properties = new Properties();
properties.setProperty("PROP1", value1);
properties.setProperty("PROP2", value2);
String renderedText = template.getText(properties);
创建新模板
文件模板的常见用例是使用特定于插件支持的语言或框架的初始内容创建新文件,则需要扩展com.intellij.createFromTemplateHandler,然后编写下面类似的代码,实现CreateFromTemplateHandler接口:
public class JavaCreateFromTemplateHandler implements CreateFromTemplateHandler {
public static PsiClass createClassOrInterface(Project project,
PsiDirectory directory,
String content,
boolean reformat,
String extension) throws IncorrectOperationException {
if (extension == null) extension = JavaFileType.INSTANCE.getDefaultExtension();
final String name = "myClass" + "." + extension;
final PsiFile psiFile = PsiFileFactory.getInstance(project).createFileFromText(name, JavaLanguage.INSTANCE, content, false, false);
psiFile.putUserData(PsiUtil.FILE_LANGUAGE_LEVEL_KEY, LanguageLevel.JDK_16);
if (!(psiFile instanceof PsiJavaFile psiJavaFile)){
throw new IncorrectOperationException("This template did not produce a Java class or an interface\n"+psiFile.getText());
}
final PsiClass[] classes = psiJavaFile.getClasses();
if (classes.length == 0) {
throw new IncorrectOperationException("This template did not produce a Java class or an interface\n"+psiFile.getText());
}
PsiClass createdClass = classes[0];
String className = createdClass.getName();
JavaDirectoryServiceImpl.checkCreateClassOrInterface(directory, className);
final LanguageLevel ll = JavaDirectoryService.getInstance().getLanguageLevel(directory);
if (ll.compareTo(LanguageLevel.JDK_1_5) < 0) {
if (createdClass.isAnnotationType()) {
throw new IncorrectOperationException("Annotations only supported at language level 1.5 and higher");
}
if (createdClass.isEnum()) {
throw new IncorrectOperationException("Enums only supported at language level 1.5 and higher");
}
}
psiJavaFile = (PsiJavaFile)psiJavaFile.setName(className + "." + extension);
PsiElement addedElement = directory.add(psiJavaFile);
if (addedElement instanceof PsiJavaFile) {
psiJavaFile = (PsiJavaFile)addedElement;
if(reformat){
CodeStyleManager.getInstance(project).scheduleReformatWhenSettingsComputed(psiJavaFile);
}
return psiJavaFile.getClasses()[0];
}
else {
PsiFile containingFile = addedElement.getContainingFile();
throw new IncorrectOperationException("Selected class file name '" +
containingFile.getName() + "' mapped to not java file type '"+
containingFile.getFileType().getDescription() + "'");
}
}
static void hackAwayEmptyPackage(PsiJavaFile file, FileTemplate template, Map<String, Object> props) throws IncorrectOperationException {
if (!template.isTemplateOfType(JavaFileType.INSTANCE)) return;
String packageName = (String)props.get(FileTemplate.ATTRIBUTE_PACKAGE_NAME);
if(packageName == null || packageName.length() == 0 || packageName.equals(FileTemplate.ATTRIBUTE_PACKAGE_NAME)){
PsiPackageStatement packageStatement = file.getPackageStatement();
if (packageStatement != null) {
packageStatement.delete();
}
}
}
@Override
public boolean handlesTemplate(@NotNull FileTemplate template) {
FileType fileType = FileTypeManagerEx.getInstanceEx().getFileTypeByExtension(template.getExtension());
return fileType.equals(JavaFileType.INSTANCE) && !ArrayUtil.contains(template.getName(), JavaTemplateUtil.INTERNAL_FILE_TEMPLATES);
}
@NotNull
@Override
public PsiElement createFromTemplate(@NotNull Project project,
@NotNull PsiDirectory directory,
String fileName,
@NotNull FileTemplate template,
@NotNull String templateText,
@NotNull Map<String, Object> props) throws IncorrectOperationException {
String extension = template.getExtension();
PsiElement result = createClassOrInterface(project, directory, templateText, template.isReformatCode(), extension);
hackAwayEmptyPackage((PsiJavaFile)result.getContainingFile(), template, props);
return result;
}
@Override
public boolean canCreate(final PsiDirectory @NotNull [] dirs) {
for (PsiDirectory dir : dirs) {
if (canCreate(dir)) return true;
}
return false;
}
@Override
public boolean isNameRequired() {
return false;
}
@NotNull
@Override
public String getErrorMessage() {
return JavaBundle.message("title.cannot.create.class");
}
@Override
public void prepareProperties(@NotNull Map<String, Object> props) {
String packageName = (String)props.get(FileTemplate.ATTRIBUTE_PACKAGE_NAME);
if (packageName == null || packageName.length() == 0) {
props.put(FileTemplate.ATTRIBUTE_PACKAGE_NAME, FileTemplate.ATTRIBUTE_PACKAGE_NAME);
}
}
@NotNull
@Override
public String commandName(@NotNull FileTemplate template) {
return JavaBundle.message("command.create.class.from.template");
}
public static boolean canCreate(PsiDirectory dir) {
return JavaDirectoryService.getInstance().getPackage(dir) != null;
}
}
公开新模板
如果想让模板出现在File | New File菜单中,必须要实现CreateFileFromTemplateAction接口,然后注册。
<!--要注册在NewGroup组下-->
<actions>
<action id="Create.MyClass" class="com.example.CreateMyClassAction">
<add-to-group group-id="NewGroup"/>
</action>
</actions>
public class CreateMyClassAction extends CreateFileFromTemplateAction {
public CreateMyClassAction() {
super("My Class", "Creates new class", CLASS_ICON);
}
@Override
protected void buildDialog(Project project, PsiDirectory directory,
CreateFileFromTemplateDialog.Builder builder) {
builder
.setTitle("New My File")
.addKind("Class", CLASS_ICON, "My Class");
}
@Override
protected String getActionName(PsiDirectory directory,
@NotNull String newName, String templateName) {
return "Create My Class: " + newName;
}
}
自定义从模板创建文件
在某些情况下,从模板创建文件的默认机制是不够的。考虑一种定义多种类型核心实体的语言,例如,在Java 语言中,可以创建以下实体:Class、Interface、Record、Enum 和Annotation。它就是把与特定语言相关的创建模板全部放在了一个分组中,如下:
这需要实现上面CreateFileFromTemplateAction类中的以下方法:
protected void buildDialog(Project project, PsiDirectory directory,
CreateFileFromTemplateDialog.Builder builder) {
builder
.setTitle("My File")
.addKind("Class", CLASS_ICON, "My Class")
.addKind("Record", RECORD_ICON, "My Record")
.addKind("Enum", ENUM_ICON, "My Enum");
}
当模板类型为Internal时(放在fileTemplates/internal中的模板),是不能公开显示的,可以通过扩展com.intellij.internalFileTemplate来向外公开在UI中显示。
<internalFileTemplate name="My Record"/>
比如,用于 Kotlin 文件创建操作的实现类如下:
internal class NewKotlinFileAction : AbstractNewKotlinFileAction(), DumbAware {
override fun isAvailable(dataContext: DataContext): Boolean {
if (!super.isAvailable(dataContext)) return false
val ideView = LangDataKeys.IDE_VIEW.getData(dataContext) ?: return false
val project = CommonDataKeys.PROJECT.getData(dataContext) ?: return false
val projectFileIndex = ProjectRootManager.getInstance(project).fileIndex
return ideView.directories.any {
projectFileIndex.isInSourceContent(it.virtualFile) ||
CreateTemplateInPackageAction.isInContentRoot(it.virtualFile, projectFileIndex)
}
}
override fun buildDialog(project: Project, directory: PsiDirectory, builder: CreateFileFromTemplateDialog.Builder) {
val sealedTemplatesEnabled = RegistryManager.getInstance().`is`("kotlin.create.sealed.templates.enabled")
builder.setTitle(KotlinBundle.message("action.new.file.dialog.title"))
builder
.addKind(
KotlinBundle.message("action.new.file.dialog.class.title"),
KotlinIcons.CLASS,
"Kotlin Class"
)
.addKind(
KotlinBundle.message("action.new.file.dialog.file.title"),
KotlinFileType.INSTANCE.icon,
"Kotlin File"
)
.addKind(
KotlinBundle.message("action.new.file.dialog.interface.title"),
KotlinIcons.INTERFACE,
"Kotlin Interface"
)
if (sealedTemplatesEnabled && project.languageVersionSettings.supportsFeature(LanguageFeature.SealedInterfaces)) {
builder.addKind(
KotlinBundle.message("action.new.file.dialog.sealed.interface.title"),
KotlinIcons.INTERFACE,
"Kotlin Sealed Interface"
)
}
builder
.addKind(
KotlinBundle.message("action.new.file.dialog.data.class.title"),
KotlinIcons.CLASS,
"Kotlin Data Class"
)
.addKind(
KotlinBundle.message("action.new.file.dialog.enum.title"),
KotlinIcons.ENUM,
"Kotlin Enum"
)
if (sealedTemplatesEnabled) {
builder.addKind(
KotlinBundle.message("action.new.file.dialog.sealed.class.title"),
KotlinIcons.CLASS,
"Kotlin Sealed Class"
)
}
builder
.addKind(
KotlinBundle.message("action.new.file.dialog.annotation.title"),
KotlinIcons.ANNOTATION,
"Kotlin Annotation"
)
builder
.addKind(
KotlinBundle.message("action.new.script.name"),
KotlinIcons.SCRIPT,
KOTLIN_SCRIPT_TEMPLATE_NAME
)
.addKind(
KotlinBundle.message("action.new.worksheet.name"),
KotlinIcons.SCRIPT,
KOTLIN_WORKSHEET_TEMPLATE_NAME
)
builder
.addKind(
KotlinBundle.message("action.new.file.dialog.object.title"),
KotlinIcons.OBJECT,
"Kotlin Object"
)
builder.setValidator(NewKotlinFileNameValidator)
}
override fun getActionName(directory: PsiDirectory, newName: String, templateName: String): String =
KotlinBundle.message("action.Kotlin.NewFile.text")
override fun hashCode(): Int = 0
override fun equals(other: Any?): Boolean = other is NewKotlinFileAction
}
internal abstract class AbstractNewKotlinFileAction : CreateFileFromTemplateAction() {
private fun KtFile.editor(): Editor? =
FileEditorManager.getInstance(this.project).selectedTextEditor?.takeIf { it.document == this.viewProvider.document }
override fun postProcess(createdElement: PsiFile, templateName: String?, customProperties: Map<String, String>?) {
super.postProcess(createdElement, templateName, customProperties)
val module = ModuleUtilCore.findModuleForPsiElement(createdElement)
if (createdElement is KtFile) {
if (module != null) {
for (hook in NewKotlinFileHook.EP_NAME.extensions) {
hook.postProcess(createdElement, module)
}
}
val ktClass = createdElement.declarations.singleOrNull() as? KtNamedDeclaration
if (ktClass != null) {
if (ktClass is KtClass && ktClass.isData()) {
val primaryConstructor = ktClass.primaryConstructor
if (primaryConstructor != null) {
createdElement.editor()?.caretModel?.moveToOffset(primaryConstructor.startOffset + 1)
return
}
}
CreateFromTemplateAction.moveCaretAfterNameIdentifier(ktClass)
} else {
val editor = createdElement.editor() ?: return
val lineCount = editor.document.lineCount
if (lineCount > 0) {
editor.caretModel.moveToLogicalPosition(LogicalPosition(lineCount - 1, 0))
}
}
}
}
override fun startInWriteAction() = false
override fun createFileFromTemplate(name: String, template: FileTemplate, dir: PsiDirectory): PsiFile? {
val targetTemplate = if (KOTLIN_WORKSHEET_TEMPLATE_NAME != template.name) {
template
} else {
object : FileTemplate by template {
override fun getExtension(): String = KOTLIN_WORKSHEET_EXTENSION
}
}
return createFileFromTemplateWithStat(name, targetTemplate, dir)
}
}
@ApiStatus.Internal
object NewKotlinFileNameValidator : InputValidatorEx {
override fun getErrorText(inputString: String): String? {
if (inputString.trim().isEmpty()) {
return KotlinBundle.message("action.new.file.error.empty.name")
}
val parts: List<String> = inputString.split(*FQNAME_SEPARATORS)
if (parts.any { it.trim().isEmpty() }) {
return KotlinBundle.message("action.new.file.error.empty.name.part")
}
return null
}
override fun checkInput(inputString: String): Boolean = true
override fun canClose(inputString: String): Boolean = getErrorText(inputString) == null
}
private fun findOrCreateTarget(dir: PsiDirectory, name: String, directorySeparators: CharArray): Pair<String, PsiDirectory> {
var className = removeKotlinExtensionIfPresent(name)
var targetDir = dir
for (splitChar in directorySeparators) {
if (splitChar in className) {
val names = className.trim().split(splitChar)
for (dirName in names.dropLast(1)) {
targetDir = targetDir.findSubdirectory(dirName) ?: runWriteAction {
targetDir.createSubdirectory(dirName)
}
}
className = names.last()
break
}
}
return Pair(className, targetDir)
}
const val KOTLIN_WORKSHEET_EXTENSION: String = "ws.kts"
internal const val KOTLIN_WORKSHEET_TEMPLATE_NAME: String = "Kotlin Worksheet"
internal const val KOTLIN_SCRIPT_TEMPLATE_NAME: String = "Kotlin Script"
private fun removeKotlinExtensionIfPresent(name: String): String = when {
name.endsWith(".$KOTLIN_WORKSHEET_EXTENSION") -> name.removeSuffix(".$KOTLIN_WORKSHEET_EXTENSION")
name.endsWith(".$STD_SCRIPT_SUFFIX") -> name.removeSuffix(".$STD_SCRIPT_SUFFIX")
name.endsWith(".${KotlinFileType.EXTENSION}") -> name.removeSuffix(".${KotlinFileType.EXTENSION}")
else -> name
}
private fun createKotlinFileFromTemplate(dir: PsiDirectory, className: String, template: FileTemplate): PsiFile? {
val project = dir.project
val defaultProperties = FileTemplateManager.getInstance(project).defaultProperties
val properties = Properties(defaultProperties)
val element = try {
CreateFromTemplateDialog(
project, dir, template,
AttributesDefaults(className).withFixedName(true),
properties
).create()
} catch (e: IncorrectOperationException) {
throw e
} catch (e: Exception) {
logger<NewKotlinFileAction>().error(e)
return null
}
return element?.containingFile
}
private val FILE_SEPARATORS: CharArray = charArrayOf('/', '\\')
private val FQNAME_SEPARATORS: CharArray = charArrayOf('/', '\\', '.')
internal fun createFileFromTemplateWithStat(name: String, template: FileTemplate, dir: PsiDirectory): PsiFile? {
KotlinCreateFileFUSCollector.logFileTemplate(template.name)
return createKotlinFileFromTemplate(name, template, dir)
}
internal fun createKotlinFileFromTemplate(name: String, template: FileTemplate, dir: PsiDirectory): PsiFile? {
val directorySeparators = when (template.name) {
"Kotlin File" -> FILE_SEPARATORS
"Kotlin Worksheet" -> FILE_SEPARATORS
"Kotlin Script" -> FILE_SEPARATORS
else -> FQNAME_SEPARATORS
}
val (className, targetDir) = findOrCreateTarget(dir, name, directorySeparators)
val service = DumbService.getInstance(dir.project)
return service.computeWithAlternativeResolveEnabled<PsiFile?, Throwable> {
val adjustedDir = CreateTemplateInPackageAction.adjustDirectory(targetDir, JavaModuleSourceRootTypes.SOURCES)
val psiFile = createKotlinFileFromTemplate(adjustedDir, className, template)
if (psiFile is KtFile) {
val singleClass = psiFile.declarations.singleOrNull() as? KtClass
if (singleClass != null && !singleClass.isEnum() && !singleClass.isInterface() && name.contains("Abstract")) {
runWriteAction {
singleClass.addModifier(KtTokens.ABSTRACT_KEYWORD)
}
}
}
JavaCreateTemplateInPackageAction.setupJdk(adjustedDir, psiFile)
val module = ModuleUtil.findModuleForFile(psiFile)
val configurator = KotlinProjectConfigurator.EP_NAME.extensions.firstOrNull()
if (module != null && configurator != null) {
DumbService.getInstance(module.project).runWhenSmart {
if (configurator.getStatus(module.toModuleGroup()) == ConfigureKotlinStatus.CAN_BE_CONFIGURED) {
configurator.configure(module.project, emptyList())
}
}
}
return@computeWithAlternativeResolveEnabled psiFile
}
}