Rename 重构操作与Find Usages的重构操作非常相似。它使用相同的规则来定位要重命名的元素,并使用相同的单词索引来查找可能引用了被重命名元素的文件。执行重命名重构时,调用方法PsiNamedElement.setName()会为重命名的元素,调用该方法PsiReference.handleElementRename()为所有对重命名元素的引用。
这些方法基本上执行相同的操作:将 PSI 元素的底层 AST 节点替换为包含用户输入的新文本的节点。从头开始创建一个完全正确的 AST 节点非常棘手。因此,获得替换节点的最简单方法是用自定义语言创建一个虚拟文件,以便它在其解析树中包含必要的节点,构建解析树并从中提取所需的节点。
java
public class PropertyImpl extends PropertiesStubElementImpl<PropertyStub> implements Property, PsiLanguageInjectionHost, PsiNameIdentifierOwner {
private static final Logger LOG = Logger.getInstance(PropertyImpl.class);
private static final Pattern PROPERTIES_SEPARATOR = Pattern.compile("^\\s*\\n\\s*\\n\\s*$");
public PropertyImpl(@NotNull ASTNode node) {
super(node);
}
public PropertyImpl(final PropertyStub stub, final IStubElementType nodeType) {
super(stub, nodeType);
}
@Override
public String toString() {
return "Property{ key = " + getKey() + ", value = " + getValue() + "}";
}
@Override
public PsiElement setName(@NotNull String name) throws IncorrectOperationException {
PropertyImpl property = (PropertyImpl)PropertiesElementFactory.createProperty(getProject(), name, "xxx", null);
ASTNode keyNode = getKeyNode();
ASTNode newKeyNode = property.getKeyNode();
LOG.assertTrue(newKeyNode != null);
if (keyNode == null) {
getNode().addChild(newKeyNode);
}
else {
getNode().replaceChild(keyNode, newKeyNode);
}
return this;
}
@Override
public void setValue(@NotNull String value) throws IncorrectOperationException {
setValue(value, PropertyKeyValueFormat.PRESENTABLE);
}
@Override
public void setValue(@NotNull String value, @NotNull PropertyKeyValueFormat format) throws IncorrectOperationException {
ASTNode node = getValueNode();
PropertyImpl property = (PropertyImpl)PropertiesElementFactory.createProperty(getProject(), "xxx", value, getKeyValueDelimiter(), format);
ASTNode valueNode = property.getValueNode();
if (node == null) {
if (valueNode != null) {
getNode().addChild(valueNode);
}
}
else {
if (valueNode == null) {
getNode().removeChild(node);
}
else {
getNode().replaceChild(node, valueNode);
}
}
}
@Override
public String getName() {
return getUnescapedKey();
}
@Override
public String getKey() {
final PropertyStub stub = getStub();
if (stub != null) {
return stub.getKey();
}
final ASTNode node = getKeyNode();
if (node == null) {
return null;
}
return node.getText();
}
@Nullable
public ASTNode getKeyNode() {
return getNode().findChildByType(PropertiesTokenTypes.KEY_CHARACTERS);
}
@Nullable
public ASTNode getValueNode() {
return getNode().findChildByType(PropertiesTokenTypes.VALUE_CHARACTERS);
}
@Override
public String getValue() {
final ASTNode node = getValueNode();
if (node == null) {
return "";
}
return node.getText();
}
@Override
@Nullable
public String getUnescapedValue() {
return unescape(getValue());
}
@Override
public @Nullable PsiElement getNameIdentifier() {
final ASTNode node = getKeyNode();
return node == null ? null : node.getPsi();
}
public static String unescape(String s) {
if (s == null) return null;
StringBuilder sb = new StringBuilder();
parseCharacters(s, sb, null);
return sb.toString();
}
public static boolean parseCharacters(String s, StringBuilder outChars, int @Nullable [] sourceOffsets) {
assert sourceOffsets == null || sourceOffsets.length == s.length() + 1;
int off = 0;
int len = s.length();
boolean result = true;
final int outOffset = outChars.length();
while (off < len) {
char aChar = s.charAt(off++);
if (sourceOffsets != null) {
sourceOffsets[outChars.length() - outOffset] = off - 1;
sourceOffsets[outChars.length() + 1 - outOffset] = off;
}
if (aChar == '\\') {
aChar = s.charAt(off++);
if (aChar == 'u') {
// Read the xxxx
int value = 0;
boolean error = false;
for (int i = 0; i < 4 && off < s.length(); i++) {
aChar = s.charAt(off++);
switch (aChar) {
case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' -> value = (value << 4) + aChar - '0';
case 'a', 'b', 'c', 'd', 'e', 'f' -> value = (value << 4) + 10 + aChar - 'a';
case 'A', 'B', 'C', 'D', 'E', 'F' -> value = (value << 4) + 10 + aChar - 'A';
default -> {
outChars.append("\\u");
int start = off - i - 1;
int end = Math.min(start + 4, s.length());
outChars.append(s, start, end);
i = 4;
error = true;
off = end;
}
}
}
if (!error) {
outChars.append((char)value);
}
else {
result = false;
}
}
else if (aChar == '\n') {
// escaped linebreak: skip whitespace in the beginning of next line
while (off < len && (s.charAt(off) == ' ' || s.charAt(off) == '\t')) {
off++;
}
}
else if (aChar == 't') {
outChars.append('\t');
}
else if (aChar == 'r') {
outChars.append('\r');
}
else if (aChar == 'n') {
outChars.append('\n');
}
else if (aChar == 'f') {
outChars.append('\f');
}
else {
outChars.append(aChar);
}
}
else {
outChars.append(aChar);
}
if (sourceOffsets != null) {
sourceOffsets[outChars.length() - outOffset] = off;
}
}
return result;
}
@Nullable
public static TextRange trailingSpaces(String s) {
if (s == null) {
return null;
}
int off = 0;
int len = s.length();
int startSpaces = -1;
while (off < len) {
char aChar = s.charAt(off++);
if (aChar == '\\') {
if (startSpaces == -1) startSpaces = off-1;
aChar = s.charAt(off++);
if (aChar == 'u') {
// Read the xxxx
int value = 0;
boolean error = false;
for (int i = 0; i < 4; i++) {
aChar = off < s.length() ? s.charAt(off++) : 0;
switch (aChar) {
case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' -> value = (value << 4) + aChar - '0';
case 'a', 'b', 'c', 'd', 'e', 'f' -> value = (value << 4) + 10 + aChar - 'a';
case 'A', 'B', 'C', 'D', 'E', 'F' -> value = (value << 4) + 10 + aChar - 'A';
default -> {
int start = off - i - 1;
int end = Math.min(start + 4, s.length());
i = 4;
error = true;
off = end;
startSpaces = -1;
}
}
}
if (!error) {
if (Character.isWhitespace(value)) {
if (startSpaces == -1) {
startSpaces = off-1;
}
}
else {
startSpaces = -1;
}
}
}
else if (aChar == '\n') {
// escaped linebreak: skip whitespace in the beginning of next line
while (off < len && (s.charAt(off) == ' ' || s.charAt(off) == '\t')) {
off++;
}
}
else if (aChar == 't' || aChar == 'r') {
if (startSpaces == -1) startSpaces = off;
}
else {
if (aChar == 'n' || aChar == 'f') {
if (startSpaces == -1) startSpaces = off;
}
else {
if (Character.isWhitespace(aChar)) {
if (startSpaces == -1) {
startSpaces = off-1;
}
}
else {
startSpaces = -1;
}
}
}
}
else {
if (Character.isWhitespace(aChar)) {
if (startSpaces == -1) {
startSpaces = off-1;
}
}
else {
startSpaces = -1;
}
}
}
return startSpaces == -1 ? null : new TextRange(startSpaces, len);
}
@Override
@Nullable
public String getUnescapedKey() {
return unescape(getKey());
}
@Nullable
@Override
protected Icon getElementIcon(@IconFlags int flags) {
return PlatformIcons.PROPERTY_ICON;
}
@Override
public void delete() throws IncorrectOperationException {
final ASTNode parentNode = getParent().getNode();
assert parentNode != null;
ASTNode node = getNode();
ASTNode prev = node.getTreePrev();
ASTNode next = node.getTreeNext();
parentNode.removeChild(node);
if ((prev == null || prev.getElementType() == TokenType.WHITE_SPACE) && next != null &&
next.getElementType() == TokenType.WHITE_SPACE) {
parentNode.removeChild(next);
}
}
@Override
public PropertiesFile getPropertiesFile() {
PsiFile containingFile = super.getContainingFile();
if (!(containingFile instanceof PropertiesFile)) {
LOG.error("Unexpected file type of: " + containingFile.getName());
}
return (PropertiesFile)containingFile;
}
/**
* The method gets the upper edge of a {@link Property} instance which might either be
* the property itself or the first {@link PsiComment} node that is related to the property
*
* @param property the property to get the upper edge for
* @return the property itself or the first {@link PsiComment} node that is related to the property
*/
static PsiElement getEdgeOfProperty(@NotNull final Property property) {
PsiElement prev = property;
for (PsiElement node = property.getPrevSibling(); node != null; node = node.getPrevSibling()) {
if (node instanceof Property) break;
if (node instanceof PsiWhiteSpace) {
if (PROPERTIES_SEPARATOR.matcher(node.getText()).find()) break;
}
prev = node;
}
return prev;
}
@Override
public String getDocCommentText() {
final PsiElement edge = getEdgeOfProperty(this);
StringBuilder text = new StringBuilder();
for(PsiElement doc = edge; doc != this; doc = doc.getNextSibling()) {
if (doc instanceof PsiComment) {
text.append(doc.getText());
text.append("\n");
}
}
if (text.length() == 0) return null;
return text.toString();
}
@NotNull
@Override
public PsiElement getPsiElement() {
return this;
}
@Override
@NotNull
public SearchScope getUseScope() {
// property ref can occur in any file
return GlobalSearchScope.allScope(getProject());
}
@Override
public ItemPresentation getPresentation() {
return new ItemPresentation() {
@Override
public String getPresentableText() {
return getName();
}
@Override
public String getLocationString() {
return getPropertiesFile().getName();
}
@Override
public Icon getIcon(final boolean open) {
return null;
}
};
}
@Override
public boolean isValidHost() {
return true;
}
@Override
public PsiLanguageInjectionHost updateText(@NotNull String text) {
return new PropertyManipulator().handleContentChange(this, text);
}
@NotNull
@Override
public LiteralTextEscaper<? extends PsiLanguageInjectionHost> createLiteralTextEscaper() {
return new PropertyImplEscaper(this);
}
public char getKeyValueDelimiter() {
final PsiElement delimiter = findChildByType(PropertiesTokenTypes.KEY_VALUE_SEPARATOR);
if (delimiter == null) {
return ' ';
}
final String text = delimiter.getText();
LOG.assertTrue(text.length() == 1);
return text.charAt(0);
}
public void replaceKeyValueDelimiterWithDefault() {
PropertyImpl property = (PropertyImpl)PropertiesElementFactory.createProperty(getProject(), "yyy", "xxx", null);
final ASTNode newDelimiter = property.getNode().findChildByType(PropertiesTokenTypes.KEY_VALUE_SEPARATOR);
final ASTNode propertyNode = getNode();
final ASTNode oldDelimiter = propertyNode.findChildByType(PropertiesTokenTypes.KEY_VALUE_SEPARATOR);
if (areDelimitersEqual(newDelimiter, oldDelimiter)) {
return;
}
if (newDelimiter == null) {
propertyNode.replaceChild(oldDelimiter, ASTFactory.whitespace(" "));
} else {
if (oldDelimiter == null) {
propertyNode.addChild(newDelimiter, getValueNode());
final ASTNode insertedDelimiter = propertyNode.findChildByType(PropertiesTokenTypes.KEY_VALUE_SEPARATOR);
LOG.assertTrue(insertedDelimiter != null);
ASTNode currentPrev = insertedDelimiter.getTreePrev();
final List<ASTNode> toDelete = new ArrayList<>();
while (currentPrev != null && currentPrev.getElementType() == PropertiesTokenTypes.WHITE_SPACE) {
toDelete.add(currentPrev);
currentPrev = currentPrev.getTreePrev();
}
for (ASTNode node : toDelete) {
propertyNode.removeChild(node);
}
} else {
propertyNode.replaceChild(oldDelimiter, newDelimiter);
}
}
}
private static boolean areDelimitersEqual(@Nullable ASTNode node1, @Nullable ASTNode node2) {
if (node1 == null && node2 == null) return true;
if (node1 == null || node2 == null) return false;
final String text1 = node1.getText();
final String text2 = node2.getText();
return text1.equals(text2);
}
}
如果重命名的引用扩展了PsiReferenceBase,则调用ElementManipulator.handleContentChange()来执行重命名,负责处理内容更改并计算元素内引用的文本范围。要禁用特定元素的重命名,请实现com.intellij.openapi.util.Condition<T>PsiElement 类型T并将其注册到com.intellij.vetoRenameCondition扩展点。
一、名称验证
NamesValidatorRename允许插件根据自定义语言规则检查用户在对话框中输入的名称是否是有效标识符(而不是关键字)。如果插件未提供此接口的实现,则使用用于验证标识符的 Java 规则。的实现NamesValidator在扩展点中注册com.intellij.lang.namesValidator。
java
public class PropertiesNamesValidator implements NamesValidator {
@Override
public boolean isKeyword(@NotNull final String name, final Project project) {
return false;
}
@Override
public boolean isIdentifier(@NotNull final String name, final Project project) {
return true;
}
}
另一种检查方式是RenameInputValidator,不像NamesValidator
它允许您更灵活地根据方法中定义的规则检查输入的名称是否正确isInputValid()
。要确定此验证器将应用于哪些元素,请覆盖getPattern()
返回要验证的元素模式的方法。比如以下示例:
java
public class YAMLAnchorRenameInputValidator implements RenameInputValidator {
@NotNull
@Override
public ElementPattern<? extends PsiElement> getPattern() {
return psiElement(YAMLAnchor.class);
}
@Override
public boolean isInputValid(@NotNull String newName, @NotNull PsiElement element, @NotNull ProcessingContext context) {
return newName.matches("[^,\\[\\]{}\\n\\t ]+");
}
}
RenameInputValidator可以扩展为RenameInputValidatorEx覆盖默认错误消息。getErrorMessage()如果名称无效或其他情况,该方法应返回自定义错误消息null。请注意,getErrorMessage()仅当所有人都RenameInputValidator接受新名称isInputValid()并且该名称是元素语言的有效标识符时才有效,这种方式需要实现com.intellij.renameInputValidator扩展点。
java
public final class YamlKeyValueRenameInputValidator implements RenameInputValidatorEx {
private static final String IDENTIFIER_START_PATTERN = "(([^\\n\\t\\r \\-?:,\\[\\]{}#&*!|>'\"%@`])" +
"|([?:-][^\\n\\t\\r ])" +
")";
private static final String IDENTIFIER_END_PATTERN = "(([^\\n\\t\\r ]#)" +
"|([^\\n\\t\\r :#])" +
"|(:[^\\n\\t\\r ])" +
")";
// Taken from yaml.flex, NS_PLAIN_ONE_LINE_block. This may not be entirely correct, but it is less restrictive than the default names
// validator
public static final Pattern IDENTIFIER_PATTERN = Pattern.compile(
"(" + IDENTIFIER_START_PATTERN + "([ \t]*" + IDENTIFIER_END_PATTERN + ")*)|" +
"('[^\\n']*')|(\"[^\\n\"]*\")"
);
@Nullable
@Override
public String getErrorMessage(@NotNull final String newName, @NotNull final Project project) {
return IDENTIFIER_PATTERN.matcher(newName).matches() ? null : YAMLBundle.message("rename.invalid.name", newName);
}
@NotNull
@Override
public ElementPattern<? extends PsiElement> getPattern() {
return PlatformPatterns.psiElement(YAMLKeyValue.class);
}
@Override
public boolean isInputValid(@NotNull final String newName, @NotNull final PsiElement element, @NotNull final ProcessingContext context) {
return true;
}
}
二、自定义重命名UI和工作流程
可以在多个级别进一步自定义重命名重构处理。提供接口的自定义实现RenameHandler允许您完全替换 rename 重构的 UI 和工作流,并且还支持重命名根本不是PsiElement的元素。示例:用于在Properties 语言插件RenameHandler中重命名资源包
java
public class ResourceBundleFromEditorRenameHandler implements RenameHandler {
@Override
public boolean isAvailableOnDataContext(@NotNull DataContext dataContext) {
final Project project = CommonDataKeys.PROJECT.getData(dataContext);
if (project == null) {
return false;
}
final ResourceBundle bundle = ResourceBundleUtil.getResourceBundleFromDataContext(dataContext);
if (bundle == null) {
return false;
}
final FileEditor fileEditor = PlatformCoreDataKeys.FILE_EDITOR.getData(dataContext);
if (!(fileEditor instanceof ResourceBundleEditor)) {
return false;
}
final VirtualFile virtualFile = CommonDataKeys.VIRTUAL_FILE.getData(dataContext);
return virtualFile instanceof ResourceBundleAsVirtualFile;
}
@Override
public void invoke(final @NotNull Project project, Editor editor, final PsiFile file, DataContext dataContext) {
final ResourceBundleEditor resourceBundleEditor = (ResourceBundleEditor)PlatformCoreDataKeys.FILE_EDITOR.getData(dataContext);
assert resourceBundleEditor != null;
final Object selectedElement = resourceBundleEditor.getSelectedElementIfOnlyOne();
if (selectedElement != null) {
CommandProcessor.getInstance().runUndoTransparentAction(() -> {
if (selectedElement instanceof PropertiesPrefixGroup group) {
ResourceBundleRenameUtil.renameResourceBundleKeySection(getPsiElementsFromGroup(group),
group.getPresentableName(),
group.getPrefix().length() - group.getPresentableName().length());
} else if (selectedElement instanceof PropertyStructureViewElement) {
final PsiElement psiElement = ((PropertyStructureViewElement)selectedElement).getPsiElement();
ResourceBundleRenameUtil.renameResourceBundleKey(psiElement, project);
} else if (selectedElement instanceof ResourceBundleFileStructureViewElement) {
ResourceBundleRenameUtil.renameResourceBundleBaseName(((ResourceBundleFileStructureViewElement)selectedElement).getValue(), project);
} else {
throw new IllegalStateException("unsupported type: " + selectedElement.getClass());
}
});
}
}
@Override
public void invoke(@NotNull Project project, PsiElement @NotNull [] elements, DataContext dataContext) {
invoke(project, null, null, dataContext);
}
private static List<PsiElement> getPsiElementsFromGroup(final PropertiesPrefixGroup propertiesPrefixGroup) {
return ContainerUtil.mapNotNull(propertiesPrefixGroup.getChildren(), treeElement -> {
if (treeElement instanceof PropertyStructureViewElement) {
return ((PropertyStructureViewElement)treeElement).getPsiElement();
}
return null;
});
如果您对标准 UI 没问题但需要扩展重命名的默认逻辑,您可以提供接口的实现RenamePsiElementProcessor,以实现以下功能:
1、重命名与调用操作的元素不同的元素(例如,超级方法)
2、一次重命名多个元素(如果它们的名称根据您的语言逻辑链接)
3、检查名称冲突(现有名称等)
4、自定义搜索代码参考或文本参考的方式
示例:用于重命名Properties 插件语言RenamePsiElementProcessor中的属性
java
public abstract class RenamePsiElementProcessor extends RenamePsiElementProcessorBase {
@NotNull
public RenameDialog createRenameDialog(@NotNull Project project,
@NotNull PsiElement element,
@Nullable PsiElement nameSuggestionContext,
@Nullable Editor editor) {
return new RenameDialog(project, element, nameSuggestionContext, editor);
}
@Override
public RenameRefactoringDialog createDialog(@NotNull Project project,
@NotNull PsiElement element,
@Nullable PsiElement nameSuggestionContext,
@Nullable Editor editor) {
return this.createRenameDialog(project, element, nameSuggestionContext, editor);
}
@NotNull
public static RenamePsiElementProcessor forElement(@NotNull PsiElement element) {
for (RenamePsiElementProcessorBase processor : EP_NAME.getExtensionList()) {
if (processor.canProcessElement(element)) {
return (RenamePsiElementProcessor)processor;
}
}
return DEFAULT;
}
@NotNull
public static List<RenamePsiElementProcessor> allForElement(@NotNull PsiElement element) {
final List<RenamePsiElementProcessor> result = new ArrayList<>();
for (RenamePsiElementProcessorBase processor : EP_NAME.getExtensions()) {
if (processor.canProcessElement(element)) {
result.add((RenamePsiElementProcessor)processor);
}
}
return result;
}
private static class MyRenamePsiElementProcessor extends RenamePsiElementProcessor implements DefaultRenamePsiElementProcessor {
@Override
public boolean canProcessElement(@NotNull final PsiElement element) {
return true;
}
}
public static final RenamePsiElementProcessor DEFAULT = new MyRenamePsiElementProcessor();
}