Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
837aa3b49e | ||
|
|
4f32fa2b56 |
No files matched your search
@@ -2,6 +2,17 @@
|
||||
|
||||
本项目所有重要变更都记录在此文件中。
|
||||
|
||||
## [1.0.1] - 2026-09-07
|
||||
|
||||
### Added / Changed
|
||||
|
||||
- 支持 **lambda 作用域**:lambda 参数(如 `it`)也可作为源 Bean,可在 `map(it -> { ... it 复制给新对象 ... })` 中使用
|
||||
- 新增「**链式 Copy**」复选框(默认不勾选):勾选后 Target 作为**类名**,生成 `new 类名().setXxx(src.getXxx()).setYyy(src.getYyy());`;未勾选时 Target 作为 **bean 名**,逐条生成 `bean.setXxx(src.getXxx());`
|
||||
- 「**同时复制父类(继承)的属性**」改为**默认勾选**
|
||||
- 下拉框类名显示优化:类名无重名时仅显示简单名;存在重名(不同包的同名类)时显示全限定名;类名+包名完全相同的重复项去重只保留一个;全限定名过长时自动截断中间
|
||||
- 目标不再做类型解析/校验,直接按源 Bean 的 getter 属性拼出对应 set 方法
|
||||
- 增加支持本地intellijIdea构建,避免下载依赖
|
||||
|
||||
## [1.0.0] - 2026-09-05
|
||||
|
||||
### Added
|
||||
|
||||
@@ -27,7 +27,9 @@ b.setYyy(a.getYyy());
|
||||
- 对话框的两个下拉框**可编辑**:既能下拉选择,也能直接输入变量名
|
||||
- 若光标正位于某个变量上,打开对话框时会**自动预填**源/目标为那个变量
|
||||
- 光标**选中**变量触发时,生成的代码会**替换选区**,去掉先前选中的变量名
|
||||
- 提供「**同时复制父类(继承)的属性**」复选框(默认不勾选),勾选后连同父类/接口继承的 getter/setter 一起复制
|
||||
- 支持 **lambda 作用域**:不仅识别局部变量,还能识别 lambda 参数(如 `it`),可在 `list.stream()...map(it -> { ... it 复制给新对象 ... }).toList()` 中直接把 `it` 当源 Bean
|
||||
- 提供「**同时复制父类(继承)的属性**」复选框(**默认勾选**),勾选后连同父类/接口继承的 getter/setter 一起复制
|
||||
- Target 填**类名**(经 IDE 自带类检索/索引解析)即可自动生成链式 `new Target().setXxx(src.getXxx()).setYyy(src.getYyy());`(只纳入返回目标类自身的链式 setter),适合 builder / 链式赋值场景;Target 填**变量名**则逐条生成赋值语句
|
||||
|
||||
## 环境要求
|
||||
|
||||
@@ -48,7 +50,7 @@ b.setYyy(a.getYyy());
|
||||
或命令行(项目自带 wrapper,无需本机安装 Gradle):
|
||||
|
||||
```bash
|
||||
./gradlew build # 产出 build/distributions/idea-plugin-copy-bean-1.0.0.zip(已让 build 依赖 buildPlugin)
|
||||
./gradlew build # 产出 build/distributions/idea-plugin-copy-bean-1.0.1.zip(已让 build 依赖 buildPlugin)
|
||||
./gradlew buildPlugin # 等价:仅产出插件 zip
|
||||
```
|
||||
|
||||
@@ -69,7 +71,7 @@ b.setYyy(a.getYyy());
|
||||
|
||||
1. 执行 `./gradlew build` 或 `./gradlew buildPlugin`
|
||||
2. 打开 IDEA → Settings → Plugins → ⚙ → Install Plugin from Disk…
|
||||
3. 选择 `build/distributions/idea-plugin-copy-bean-1.0.0.zip` 并重启 IDE
|
||||
3. 选择 `build/distributions/idea-plugin-copy-bean-1.0.1.zip` 并重启 IDE
|
||||
|
||||
## 项目结构
|
||||
|
||||
|
||||
+21
-3
@@ -5,9 +5,15 @@ plugins {
|
||||
// 依赖仓库由 settings.gradle.kts 中的 dependencyResolutionManagement 统一配置
|
||||
dependencies {
|
||||
intellijPlatform {
|
||||
// IntelliJ IDEA Community (IC),目标平台 2025.2+(build 252)
|
||||
intellijIdea("2025.2.6.2")
|
||||
// 把内置的 Java 插件(含 Java PSI)加入编译类路径,供本插件使用 PsiClass/PsiMethod 等
|
||||
/* IntelliJ IDEA Community (IC),目标平台 2025.2+(build 252)
|
||||
会在线下载巨大的包:https://download.jetbrains.com/idea/ideaIU-2025.2.6.2.win.zip (1.90 GB)
|
||||
*/
|
||||
// intellijIdea("2025.2.6.2")
|
||||
/* 或者指向你电脑上已经安装好的 IDEA 路径,把内置的 Java 插件(含 Java PSI)加入编译类路径,供本插件使用 PsiClass/PsiMethod 等 */
|
||||
val ideaPath = System.getenv("IDEA_HOME") // 使用 System.getenv() 读取环境变量 ①
|
||||
?: providers.gradleProperty("localIdeaPath").orNull // 从 gradle.properties 中读取 ②(未配置时不抛异常)
|
||||
?: "${System.getProperty("user.home")}/AppData/Local/Programs/IntelliJ IDEA Ultimate 2" // 兜底 ③
|
||||
local(ideaPath)
|
||||
bundledPlugin("com.intellij.java")
|
||||
}
|
||||
}
|
||||
@@ -16,3 +22,15 @@ dependencies {
|
||||
tasks.named("build") {
|
||||
dependsOn("buildPlugin")
|
||||
}
|
||||
|
||||
// 本机 JDK 为 Microsoft 版 21(无 JBR 的 Packages 布局),关闭字节码插桩。
|
||||
// 仅影响 @NotNull 等注解的运行时检查,不影响本插件功能。
|
||||
tasks.named("instrumentCode") {
|
||||
enabled = false
|
||||
}
|
||||
|
||||
// 指定支持的 IDEA 版本
|
||||
tasks.withType<org.jetbrains.intellij.platform.gradle.tasks.PatchPluginXmlTask> {
|
||||
sinceBuild.set("252") // 最小版本
|
||||
// untilBuild.set("252.*") // 最大版本;如果你想让插件兼容所有后续版本,可以直接不设置
|
||||
}
|
||||
+4
-1
@@ -1,5 +1,5 @@
|
||||
group = top.yexuejc
|
||||
version = 1.0.0
|
||||
version = 1.0.1
|
||||
|
||||
org.gradle.jvmargs=-Xmx2048m
|
||||
|
||||
@@ -9,3 +9,6 @@ kotlin.stdlib.default.dependency = false
|
||||
# 启用 Gradle 配置缓存与构建缓存
|
||||
org.gradle.configuration-cache = true
|
||||
org.gradle.caching = true
|
||||
|
||||
# 配置IDEA的安装路径(用于编译,也可以不设定)
|
||||
#localIdeaPath=C:/Users/intasect/AppData/Local/Programs/IntelliJ IDEA Ultimate 2
|
||||
@@ -12,8 +12,9 @@ import com.intellij.psi.util.PsiTreeUtil
|
||||
/**
|
||||
* 触发入口:Alt+Insert(Generate) -> Bean Copy... / 右键菜单 -> Bean Copy...
|
||||
*
|
||||
* 在方法作用域内选择源 Bean A 与目标 Bean B,然后遍历 A 的所有 getter,
|
||||
* 在 B 中查找同名 setter,自动生成: b.setXxx(a.getXxx());
|
||||
* 在方法(或 lambda)作用域内选择源 Bean,目标直接用字符串(不做类型解析/校验):
|
||||
* - 未勾选「链式 Copy」:<target>.setXxx(src.getXxx()); ...(target 用作 bean 名)
|
||||
* - 勾选「链式 Copy」:new <target>().setXxx(src.getXxx()).setYyy(src.getYyy());(target 用作类名)
|
||||
*/
|
||||
class BeanCopyAction : AnAction() {
|
||||
|
||||
@@ -34,11 +35,12 @@ class BeanCopyAction : AnAction() {
|
||||
val offset = editor.caretModel.currentCaret.offset
|
||||
val beanVars = collectBeanVariables(file, offset)
|
||||
|
||||
if (beanVars.size < 2) {
|
||||
val dialogTitle = "Bean Copy"
|
||||
if (beanVars.isEmpty()) {
|
||||
Messages.showInfoMessage(
|
||||
project,
|
||||
"在当前作用域中未找到足够的 Bean 变量,请先写如:A a = new A(); B b = new B();",
|
||||
"Bean Copy"
|
||||
"在当前作用域中未找到可作为源 Bean 的变量,请先写如:A a = new A();",
|
||||
dialogTitle
|
||||
)
|
||||
return
|
||||
}
|
||||
@@ -47,23 +49,48 @@ class BeanCopyAction : AnAction() {
|
||||
val defaultName = findSelectedVariableName(file, offset)
|
||||
val dialog = BeanCopyDialog(beanVars, project, defaultName)
|
||||
if (!dialog.showAndGet()) return
|
||||
|
||||
val source = dialog.sourceVar
|
||||
val target = dialog.targetVar
|
||||
if (source == null || target == null) {
|
||||
if (source == null) {
|
||||
Messages.showInfoMessage(
|
||||
project,
|
||||
"源与目标 Bean 请选择有效变量(或输入作用域内存在、且带 getter/setter 的变量名)。",
|
||||
"Bean Copy"
|
||||
"源 Bean 请选择有效变量(或输入作用域内存在、且带 getter/setter 的变量名)。",
|
||||
dialogTitle
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
val code = generateCopyCode(source, target, dialog.copyParent)
|
||||
// 目标不做类型解析/校验:只依赖 Source 的属性拼接 set 方法。
|
||||
val code: String?
|
||||
if (dialog.chainCopy) {
|
||||
val token = dialog.targetChainToken
|
||||
if (token.isNullOrBlank()) {
|
||||
Messages.showInfoMessage(
|
||||
project,
|
||||
"请在 Target 中填写一个类名(用于生成 new 类名().setXxx(...)),例如 TransactionModel。",
|
||||
dialogTitle
|
||||
)
|
||||
return
|
||||
}
|
||||
code = generateChainCode(source, token, dialog.copyParent)
|
||||
} else {
|
||||
val name = dialog.targetBeanName
|
||||
if (name.isNullOrBlank()) {
|
||||
Messages.showInfoMessage(
|
||||
project,
|
||||
"请在 Target 中填写/选择一个目标变量名。",
|
||||
dialogTitle
|
||||
)
|
||||
return
|
||||
}
|
||||
code = generateCopyCode(source, name, dialog.copyParent)
|
||||
}
|
||||
|
||||
if (code == null) {
|
||||
Messages.showInfoMessage(
|
||||
project,
|
||||
"源与目标 Bean 之间没有可复制的公共属性。",
|
||||
"Bean Copy"
|
||||
"源 Bean 没有可复制的属性。",
|
||||
dialogTitle
|
||||
)
|
||||
return
|
||||
}
|
||||
@@ -81,40 +108,25 @@ class BeanCopyAction : AnAction() {
|
||||
|
||||
// ---------- 生成代码 ----------
|
||||
|
||||
private fun generateCopyCode(source: BeanVar, target: BeanVar, copyParent: Boolean): String? {
|
||||
val sourceClass = source.cls
|
||||
val targetClass = target.cls
|
||||
val targetName = target.variable.name ?: return null
|
||||
// 普通 Copy:按源属性逐一拼 <target>.setXxx(src.getXxx());
|
||||
private fun generateCopyCode(source: BeanVar, targetName: String, copyParent: Boolean): String? {
|
||||
// targetName 首字母小写
|
||||
val tarName = targetName.replaceFirstChar { it.lowercase() }
|
||||
val sourceName = source.variable.name ?: return null
|
||||
|
||||
// 是否把父类/接口继承的属性也纳入扫描
|
||||
val sourceClasses = if (copyParent) classHierarchy(sourceClass) else listOf(sourceClass)
|
||||
val targetClasses = if (copyParent) classHierarchy(targetClass) else listOf(targetClass)
|
||||
val sourceClasses = if (copyParent) classHierarchy(source.cls) else listOf(source.cls)
|
||||
|
||||
val sb = StringBuilder()
|
||||
var count = 0
|
||||
val processed = HashSet<String>()
|
||||
for (srcCls in sourceClasses) {
|
||||
for (getter in srcCls.methods) {
|
||||
// 只看 getter:0 个参数且符合 get/is 命名
|
||||
if (!isGetter(getter)) continue
|
||||
val property = PropertyUtil.getPropertyName(getter) ?: continue
|
||||
// 子类已处理过的同名属性(父类覆盖场景)不再重复生成
|
||||
if (!processed.add(property)) continue
|
||||
// 在目标类(及其祖先)中手工查找同名 setter(避免依赖 findPropertySetter 的平台重载差异)
|
||||
val setter = targetClasses.flatMap { it.methods.toList() }.firstOrNull {
|
||||
isSetter(it) && PropertyUtil.getPropertyName(it) == property
|
||||
} ?: continue
|
||||
if (setter.parameterList.parametersCount != 1) continue
|
||||
|
||||
sb.append(targetName)
|
||||
.append(".")
|
||||
.append(setter.name)
|
||||
.append("(")
|
||||
.append(sourceName)
|
||||
.append(".")
|
||||
.append(getter.name)
|
||||
.append("());\n")
|
||||
val setterName = setterNameFor(getter) ?: continue
|
||||
sb.append(tarName).append(".")
|
||||
.append(setterName).append("(")
|
||||
.append(sourceName).append(".").append(getter.name).append("());\n")
|
||||
count++
|
||||
}
|
||||
}
|
||||
@@ -122,6 +134,41 @@ class BeanCopyAction : AnAction() {
|
||||
return sb.toString()
|
||||
}
|
||||
|
||||
// 链式 Copy:new <target>().setXxx(src.getXxx()).setYyy(src.getYyy());
|
||||
private fun generateChainCode(source: BeanVar, targetToken: String, copyParent: Boolean): String? {
|
||||
val sourceName = source.variable.name ?: return null
|
||||
val sourceClasses = if (copyParent) classHierarchy(source.cls) else listOf(source.cls)
|
||||
|
||||
val sb = StringBuilder("new ").append(targetToken).append("()")
|
||||
var count = 0
|
||||
val processed = HashSet<String>()
|
||||
for (srcCls in sourceClasses) {
|
||||
for (getter in srcCls.methods) {
|
||||
if (!isGetter(getter)) continue
|
||||
val property = PropertyUtil.getPropertyName(getter) ?: continue
|
||||
if (!processed.add(property)) continue
|
||||
val setterName = setterNameFor(getter) ?: continue
|
||||
sb.append("\n .").append(setterName)
|
||||
.append("(").append(sourceName).append(".").append(getter.name).append("())")
|
||||
count++
|
||||
}
|
||||
}
|
||||
if (count == 0) return null
|
||||
sb.append(";")
|
||||
return sb.toString()
|
||||
}
|
||||
|
||||
// 由 getter 反推 setter 方法名:getXxx -> setXxx;isXxx -> setXxx
|
||||
private fun setterNameFor(getter: PsiMethod): String? {
|
||||
val name = getter.name
|
||||
val prop = when {
|
||||
name.startsWith("get") && name.length > 3 -> name.substring(3)
|
||||
name.startsWith("is") && name.length > 2 -> name.substring(2)
|
||||
else -> return null
|
||||
}
|
||||
return "set" + prop.replaceFirstChar { it.titlecase() }
|
||||
}
|
||||
|
||||
// 收集类 + 所有父类/接口,用于扫描继承到的属性(子类优先,自动去重)
|
||||
private fun classHierarchy(cls: PsiClass): List<PsiClass> {
|
||||
val result = LinkedHashSet<PsiClass>()
|
||||
@@ -142,18 +189,15 @@ class BeanCopyAction : AnAction() {
|
||||
return PropertyUtil.getPropertyName(m) != null
|
||||
}
|
||||
|
||||
private fun isSetter(m: PsiMethod): Boolean {
|
||||
if (m.parameterList.parametersCount != 1) return false
|
||||
if (m.hasModifierProperty(PsiModifier.STATIC)) return false
|
||||
return PropertyUtil.getPropertyName(m) != null
|
||||
}
|
||||
|
||||
// ---------- 收集作用域内的 Bean 变量 ----------
|
||||
|
||||
private fun collectBeanVariables(file: PsiFile, offset: Int): List<BeanVar> {
|
||||
val element = file.findElementAt(offset) ?: return emptyList()
|
||||
val scope = findScope(element) ?: return emptyList()
|
||||
val vars = PsiTreeUtil.collectElementsOfType(scope, PsiLocalVariable::class.java)
|
||||
// 局部变量 + lambda 参数(lambda 的 it / 命名参数是 PsiParameter,而非 PsiLocalVariable)
|
||||
val locals = PsiTreeUtil.collectElementsOfType(scope, PsiLocalVariable::class.java)
|
||||
val params = PsiTreeUtil.collectElementsOfType(scope, PsiParameter::class.java)
|
||||
val vars = locals + params
|
||||
return vars.mapNotNull { v ->
|
||||
val cls = resolveBeanClass(v.type) ?: return@mapNotNull null
|
||||
if (!hasGetterOrSetter(cls)) return@mapNotNull null
|
||||
@@ -163,12 +207,8 @@ class BeanCopyAction : AnAction() {
|
||||
|
||||
private fun findScope(element: PsiElement): PsiElement? {
|
||||
val method = PsiTreeUtil.getParentOfType(element, PsiMethod::class.java)
|
||||
if (method != null && method.body != null) return method.body
|
||||
val block = PsiTreeUtil.getParentOfType(element, PsiCodeBlock::class.java)
|
||||
if (block != null) return block
|
||||
val field = PsiTreeUtil.getParentOfType(element, PsiField::class.java)
|
||||
if (field != null) return field
|
||||
return null
|
||||
val scope = method?.body ?: PsiTreeUtil.getParentOfType(element, PsiCodeBlock::class.java)
|
||||
return scope ?: PsiTreeUtil.getParentOfType(element, PsiField::class.java)
|
||||
}
|
||||
|
||||
private fun resolveBeanClass(type: PsiType): PsiClass? {
|
||||
@@ -177,13 +217,16 @@ class BeanCopyAction : AnAction() {
|
||||
}
|
||||
|
||||
private fun hasGetterOrSetter(cls: PsiClass): Boolean =
|
||||
cls.methods.any { isGetter(it) || isSetter(it) }
|
||||
cls.methods.any { isGetter(it) || isSetterMethod(it) }
|
||||
|
||||
// 返回光标位置所在的局部变量名(用于预填源/目标下拉框)
|
||||
private fun isSetterMethod(m: PsiMethod): Boolean =
|
||||
m.parameterList.parametersCount == 1 && !m.hasModifierProperty(PsiModifier.STATIC) && PropertyUtil.getPropertyName(m) != null
|
||||
|
||||
// 返回光标位置所在的变量名 / lambda 参数名(用于预填源/目标下拉框)
|
||||
private fun findSelectedVariableName(file: PsiFile, offset: Int): String? {
|
||||
val element = file.findElementAt(offset) ?: return null
|
||||
val refVariable = file.findReferenceAt(offset)?.resolve() as? PsiLocalVariable
|
||||
val variable = refVariable ?: PsiTreeUtil.getParentOfType(element, PsiLocalVariable::class.java)
|
||||
val refVariable = file.findReferenceAt(offset)?.resolve() as? PsiVariable
|
||||
val variable = refVariable ?: PsiTreeUtil.getParentOfType(element, PsiVariable::class.java)
|
||||
return variable?.name
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,11 +9,14 @@ import javax.swing.JComponent
|
||||
import javax.swing.JPanel
|
||||
|
||||
/**
|
||||
* 选择源 Bean(读取属性)与目标 Bean(写入属性)的对话框。
|
||||
* 选择源 Bean(读取属性)与目标(写入属性)的对话框。
|
||||
*
|
||||
* 两个下拉框均可编辑:既可从列表中选择,也可直接输入变量名。
|
||||
* 若传入 [defaultName],打开时会把源/目标都预填为该变量。
|
||||
* 可通过 [copyParent] 决定是否连父类/接口继承的属性一起复制。
|
||||
* - Source / Target 下拉框均可编辑:既可从列表中选择,也可直接输入。
|
||||
* - Target 不做类型解析/校验:
|
||||
* - 未勾选「链式 Copy」时,Target 的字符串当作 **bean 名** -> 生成 `m1.setXxx(src.getXxx());`
|
||||
* - 勾选「链式 Copy」时,Target 的字符串当作 **类名** -> 生成 `new 类名().setXxx(src.getXxx()).setYyy(...)`
|
||||
* - 下拉框展示:类名无重名时仅显示简单名;有重名时显示全限定名;类名+包名重复项去重;超长截断。
|
||||
* - [copyParent] 默认勾选,复制时纳入父类/接口继承的属性。
|
||||
*/
|
||||
class BeanCopyDialog(
|
||||
private val vars: List<BeanVar>,
|
||||
@@ -21,48 +24,117 @@ class BeanCopyDialog(
|
||||
defaultName: String? = null,
|
||||
) : DialogWrapper(project, true) {
|
||||
|
||||
private val sourceBox = JComboBox(vars.toTypedArray())
|
||||
private val targetBox = JComboBox(vars.toTypedArray())
|
||||
// 去重 + 计算显示文本后的列表(下拉框项与解析都基于它)
|
||||
private val comboVars: List<BeanVar> = prepareDisplayVars()
|
||||
|
||||
private val sourceBox = JComboBox(comboVars.toTypedArray())
|
||||
private val targetBox = JComboBox(comboVars.toTypedArray())
|
||||
private val chainCheck = JCheckBox("链式 Copy:将 Target 作为类名,生成 new 类名().setXxx(...).setYyy(...)")
|
||||
private val copyParentCheck = JCheckBox("同时复制父类(继承)的属性")
|
||||
|
||||
val sourceVar: BeanVar? get() = resolveSelection(sourceBox)
|
||||
val targetVar: BeanVar? get() = resolveSelection(targetBox)
|
||||
|
||||
/** 是否连父类/接口继承的属性一起复制(默认不选中) */
|
||||
/** 是否生成链式 new 类名().set...()(默认不勾选) */
|
||||
val chainCopy: Boolean get() = chainCheck.isSelected
|
||||
|
||||
/** 是否连父类/接口继承的属性一起复制(默认勾选) */
|
||||
val copyParent: Boolean get() = copyParentCheck.isSelected
|
||||
|
||||
/** 普通模式:Target 的 bean 名(选变量 -> 变量名;输入 -> 字符串原样) */
|
||||
val targetBeanName: String? get() = targetText { it.variable.name }
|
||||
|
||||
/** 链式模式:Target 的类名 token(选变量 -> 其类型(类简单名);输入 -> 字符串原样) */
|
||||
val targetChainToken: String? get() = targetText { it.cls.name }
|
||||
|
||||
init {
|
||||
title = "Bean Copy"
|
||||
sourceBox.isEditable = true
|
||||
targetBox.isEditable = true
|
||||
copyParentCheck.isSelected = true
|
||||
prefill(defaultName)
|
||||
init()
|
||||
}
|
||||
|
||||
// 默认把源/目标都设为指定变量名对应的 BeanVar(若存在于列表中)
|
||||
/** 默认把源/目标都设为指定变量名对应的 BeanVar(若存在于列表中) */
|
||||
private fun prefill(name: String?) {
|
||||
if (name.isNullOrBlank()) return
|
||||
val matched = vars.firstOrNull { it.variable.name == name } ?: return
|
||||
val matched = comboVars.firstOrNull { it.variable.name == name } ?: return
|
||||
sourceBox.selectedItem = matched
|
||||
targetBox.selectedItem = matched
|
||||
}
|
||||
|
||||
// 解析下拉框当前值:选中的对象或直接输入的名称都能映射到 BeanVar
|
||||
/** 解析源下拉框当前值:选中的对象或直接输入的名称都能映射到 BeanVar */
|
||||
private fun resolveSelection(box: JComboBox<*>): BeanVar? {
|
||||
val sel = box.selectedItem
|
||||
return when (sel) {
|
||||
is BeanVar -> sel
|
||||
is String -> vars.firstOrNull { it.variable.name == sel }
|
||||
is String -> comboVars.firstOrNull { it.variable.name == sel }
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
/** 取 Target 文本:选中的变量按需取变量名/类名;输入的原样返回(无校验) */
|
||||
private fun targetText(selector: (BeanVar) -> String?): String? {
|
||||
val sel = targetBox.selectedItem
|
||||
return when (sel) {
|
||||
is BeanVar -> selector(sel)
|
||||
is String -> sel.trim().ifEmpty { null }
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成下拉框项:
|
||||
* 1) 类名+包名都相同(同一类)的重复项只保留一个;
|
||||
* 2) 类简单名在本作用域内出现多次(不同包的同名类)时,这些项显示全限定名,其余仅显示简单名。
|
||||
*/
|
||||
private fun prepareDisplayVars(): List<BeanVar> {
|
||||
// 按类的唯一标识去重(qualifiedName 相同视为同一类)
|
||||
val deduped = LinkedHashMap<String, BeanVar>()
|
||||
for (v in vars) {
|
||||
val key = v.cls.qualifiedName ?: v.cls.name ?: continue
|
||||
deduped.putIfAbsent(key, v)
|
||||
}
|
||||
val list = deduped.values.toList()
|
||||
|
||||
// 统计类简单名的出现次数,判断哪些需要显示全限定名
|
||||
val simpleNameCount = HashMap<String, Int>()
|
||||
for (v in list) {
|
||||
val s = v.cls.name ?: continue
|
||||
simpleNameCount[s] = (simpleNameCount[s] ?: 0) + 1
|
||||
}
|
||||
|
||||
for (v in list) {
|
||||
val simple = v.cls.name ?: continue
|
||||
val varName = v.variable.name ?: "?"
|
||||
v.display = if (simpleNameCount[simple]!! > 1) {
|
||||
val fq = v.cls.qualifiedName ?: simple
|
||||
"$varName : ${truncateFq(fq)}"
|
||||
} else {
|
||||
"$varName : $simple"
|
||||
}
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
/** 全限定名过长时截断中间(保留首尾段与类名),避免下拉框被撑宽 */
|
||||
private fun truncateFq(fq: String, max: Int = 56): String {
|
||||
if (fq.length <= max) return fq
|
||||
val simple = fq.substringAfterLast('.')
|
||||
val pkg = fq.substringBeforeLast('.')
|
||||
val budget = max - simple.length - 1
|
||||
if (budget <= 0) return simple
|
||||
val keep = budget / 2
|
||||
return "${pkg.take(keep)}…${pkg.takeLast(keep)}.$simple"
|
||||
}
|
||||
|
||||
override fun createCenterPanel(): JComponent {
|
||||
return FormBuilder.createFormBuilder()
|
||||
.addLabeledComponent("Source Bean (读取属性):", sourceBox)
|
||||
.addLabeledComponent("Target Bean (写入属性):", targetBox)
|
||||
.addComponent(chainCheck)
|
||||
.addComponent(copyParentCheck)
|
||||
.addComponentFillVertically(JPanel(), 0)
|
||||
.panel
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,20 @@
|
||||
package top.yexuejc.beancopy
|
||||
|
||||
import com.intellij.psi.PsiClass
|
||||
import com.intellij.psi.PsiLocalVariable
|
||||
import com.intellij.psi.PsiVariable
|
||||
|
||||
/**
|
||||
* 作用域内的一个 Bean 变量(局部变量 + 其解析出的类)。
|
||||
* 作用域内的一个 Bean 变量(局部变量或 lambda 参数 + 其解析出的类)。
|
||||
*
|
||||
* 在对话框的下拉框中以下列文本显示:
|
||||
* <code>varName : ClassSimpleName</code>
|
||||
* 下拉框中显示的文本由 [display] 决定:
|
||||
* - 类名在本作用域内**无重名**时,显示 <code>varName : ClassSimpleName</code>
|
||||
* - 类名**有重名**时,显示 <code>varName : 全限定名</code>(用于区分不同包下的同名类)
|
||||
*/
|
||||
class BeanVar(val variable: PsiLocalVariable, val cls: PsiClass) {
|
||||
override fun toString(): String = "${variable.name} : ${cls.name}"
|
||||
}
|
||||
class BeanVar(val variable: PsiVariable, val cls: PsiClass) {
|
||||
|
||||
/** 下拉框显示文本;由 [top.yexuejc.beancopy.BeanCopyDialog] 按重名/去重情况计算后赋值 */
|
||||
var display: String? = null
|
||||
|
||||
override fun toString(): String =
|
||||
display ?: "${variable.name} : ${cls.name}"
|
||||
}
|
||||
@@ -7,17 +7,16 @@
|
||||
<b>Bean Copy</b> 一键把源 JavaBean 的属性值通过 get/set 方法复制到目标 JavaBean 上。
|
||||
<br>
|
||||
用法:在方法体内写 <code>A a = new A(); B b = new B();</code>,将光标置于方法内,<br>
|
||||
按 <b>Alt+Insert</b> (Generate) 选择 <b>Bean Copy...</b>,在弹出的对话框中<br>
|
||||
按 <b>Alt+Insert</b> (Generate) 或者<b>右键</b>选择 <b>Bean Copy...</b>,在弹出的对话框中<br>
|
||||
选择源 Bean 与目标 Bean,即可自动生成:<br>
|
||||
<code>b.setXxx(a.getXxx());</code> 等所有可复制的赋值语句。
|
||||
<code>b.setXxx(a.getXxx());</code> 等所有可复制的赋值语句。<br>
|
||||
支持 lambda 作用域(把 lambda 的 <code>it</code> 参数当源 Bean);Target 填写<br>
|
||||
类名(经 IDE 类检索)即自动生成链式 <code>new Target().setXxx(...).setYyy(...);</code>。
|
||||
]]></description>
|
||||
|
||||
<depends>com.intellij.modules.platform</depends>
|
||||
<depends>com.intellij.modules.java</depends>
|
||||
|
||||
<!-- 支持 2025.2+ 平台(build 252) -->
|
||||
<idea-version since-build="252" />
|
||||
|
||||
<extensions defaultExtensionNs="com.intellij">
|
||||
</extensions>
|
||||
|
||||
|
||||
Reference in new issue
Block a user