/* * Copyright (c) 2019, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.wso2.lsp4intellij.contributors.psi; import com.intellij.lang.ASTNode; import com.intellij.lang.Language; import com.intellij.navigation.ItemPresentation; import com.intellij.openapi.editor.Editor; import com.intellij.openapi.fileEditor.FileEditorManager; import com.intellij.openapi.fileEditor.OpenFileDescriptor; import com.intellij.openapi.fileTypes.PlainTextLanguage; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.Key; import com.intellij.openapi.util.KeyWithDefaultValue; import com.intellij.openapi.util.TextRange; import com.intellij.openapi.util.UserDataHolderBase; import com.intellij.psi.ContributedReferenceHost; import com.intellij.psi.FileViewProvider; import com.intellij.psi.NavigatablePsiElement; import com.intellij.psi.PsiDocumentManager; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiElementVisitor; import com.intellij.psi.PsiFile; import com.intellij.psi.PsiInvalidElementAccessException; import com.intellij.psi.PsiManager; import com.intellij.psi.PsiNameIdentifierOwner; import com.intellij.psi.PsiNamedElement; import com.intellij.psi.PsiPolyVariantReference; import com.intellij.psi.PsiReference; import com.intellij.psi.PsiReferenceService; import com.intellij.psi.ResolveState; import com.intellij.psi.scope.PsiScopeProcessor; import com.intellij.psi.search.GlobalSearchScope; import com.intellij.psi.search.SearchScope; import com.intellij.util.IncorrectOperationException; import com.intellij.util.concurrency.AtomicFieldUpdater; import com.intellij.util.keyFMap.KeyFMap; import org.jetbrains.annotations.NotNull; import org.wso2.lsp4intellij.utils.ApplicationUtils; import org.wso2.lsp4intellij.utils.FileUtils; import javax.annotation.Nullable; import javax.swing.*; /** * A simple PsiElement for LSP */ public class LSPPsiElement implements PsiNameIdentifierOwner, NavigatablePsiElement { private final Key<KeyFMap> COPYABLE_USER_MAP_KEY = Key.create("COPYABLE_USER_MAP_KEY"); private final AtomicFieldUpdater<LSPPsiElement, KeyFMap> updater = AtomicFieldUpdater.forFieldOfType(LSPPsiElement.class, KeyFMap.class); private final PsiManager manager; private final LSPPsiReference reference; private final Project project; private String name; private final PsiFile file; public final int start; public final int end; /** * @param name The name (text) of the element * @param project The project it belongs to * @param start The offset in the editor where the element starts * @param end The offset where it ends */ public LSPPsiElement(String name, @NotNull Project project, int start, int end, PsiFile file) { this.project = project; this.name = name; this.start = start; this.end = end; this.file = file; manager = PsiManager.getInstance(project); reference = new LSPPsiReference(this); } /** * Concurrent writes to this field are via CASes only, using the {@link #updater} */ private volatile KeyFMap myUserMap = KeyFMap.EMPTY_MAP; /** * Returns the language of the PSI element. * * @return the language instance. */ @NotNull public Language getLanguage() { return PlainTextLanguage.INSTANCE; } /** * Returns the PSI manager for the project to which the PSI element belongs. * * @return the PSI manager instance. */ public PsiManager getManager() { return manager; } /** * Returns the array of children for the PSI element. Important: In some implementations children are only composite * elements, i.e. not a leaf elements * * @return the array of child elements. */ @NotNull public PsiElement[] getChildren() { return new PsiElement[0]; } /** * Returns the parent of the PSI element. * * @return the parent of the element, or null if the element has no parent. */ public PsiElement getParent() { return getContainingFile(); } /** * Returns the first child of the PSI element. * * @return the first child, or null if the element has no children. */ public PsiElement getFirstChild() { return null; } /** * Returns the last child of the PSI element. * * @return the last child, or null if the element has no children. */ public PsiElement getLastChild() { return null; } /** * Returns the next sibling of the PSI element. * * @return the next sibling, or null if the node is the last in the list of siblings. */ public PsiElement getNextSibling() { return null; } /** * Returns the previous sibling of the PSI element. * * @return the previous sibling, or null if the node is the first in the list of siblings. */ public PsiElement getPrevSibling() { return null; } /** * Returns the text range in the document occupied by the PSI element. * * @return the text range. */ public TextRange getTextRange() { return new TextRange(start, end); } /** * Returns the text offset of the PSI element relative to its parent. * * @return the relative offset. */ public int getStartOffsetInParent() { return start; } /** * Returns the length of text of the PSI element. * * @return the text length. */ public int getTextLength() { return end - start; } /** * Finds a leaf PSI element at the specified offset from the start of the text range of this node. * * @param offset the relative offset for which the PSI element is requested. * @return the element at the offset, or null if none is found. */ public PsiElement findElementAt(int offset) { return null; } /** * Finds a reference at the specified offset from the start of the text range of this node. * * @param offset the relative offset for which the reference is requested. * @return the reference at the offset, or null if none is found. */ public PsiReference findReferenceAt(int offset) { return null; } /** * Returns the text of the PSI element as a character array. * * @return the element text as a character array. */ @NotNull public char[] textToCharArray() { return name.toCharArray(); } /** * Returns the PSI element which should be used as a navigation target when navigation to this PSI element is * requested. The method can either return {@code this} or substitute a different element if this element does not * have an associated file and offset. (For example, if the source code of a library is attached to a project, the * navigation element for a compiled library class is its source class.) * * @return the navigation target element. */ public PsiElement getNavigationElement() { return this; } /** * Returns the PSI element which corresponds to this element and belongs to either the project source path or class * path. The method can either return {@code this} or substitute a different element if this element does not belong * to the source path or class path. (For example, the original element for a library source file is the * corresponding compiled class file.) * * @return the original element. */ public PsiElement getOriginalElement() { return null; } /** * Checks if the text of this PSI element is equal to the specified character sequence. * * @param text the character sequence to compare with. * @return true if the text is equal, false otherwise. */ public boolean textMatches(@NotNull CharSequence text) { return getText() == text; } //Q: get rid of these methods? /** * Checks if the text of this PSI element is equal to the text of the specified PSI element. * * @param element the element to compare the text with. * @return true if the text is equal, false otherwise. */ public boolean textMatches(PsiElement element) { return getText().equals(element.getText()); } /** * Checks if the text of this element contains the specified character. * * @param c the character to search for. * @return true if the character is found, false otherwise. */ public boolean textContains(char c) { return getText().indexOf(c) >= 0; } /** * Returns the text of the PSI element. * * @return the element text. */ public String getText() { return name; } /** * Passes the element to the specified visitor. * * @param visitor the visitor to pass the element to. */ public void accept(PsiElementVisitor visitor) { visitor.visitElement(this); } /** * Passes the children of the element to the specified visitor. * * @param visitor the visitor to pass the children to. */ public void acceptChildren(@NotNull PsiElementVisitor visitor) { } /** * Creates a copy of the file containing the PSI element and returns the corresponding element in the created copy. * Resolve operations performed on elements in the copy of the file will resolve to elements in the copy, not in the * original file. * * @return the element in the file copy corresponding to this element. */ public PsiElement copy() { return null; } /** * Adds a child to this PSI element. * * @param element the child element to add. * @return the element which was actually added (either { @code element} or its copy). * @throws IncorrectOperationException if the modification is not supported or not possible for some reason. */ public PsiElement add(@NotNull PsiElement element) { throw new IncorrectOperationException(); } /** * Adds a child to this PSI element, before the specified anchor element. * * @param element the child element to add. * @param anchor the anchor before which the child element is inserted (must be a child of this PSI element) * @return the element which was actually added (either { @code element} or its copy). * @throws IncorrectOperationException if the modification is not supported or not possible for some reason. */ public PsiElement addBefore(@NotNull PsiElement element, PsiElement anchor) { throw new IncorrectOperationException(); } /** * Adds a child to this PSI element, after the specified anchor element. * * @param element the child element to add. * @param anchor the anchor after which the child element is inserted (must be a child of this PSI element) * @return the element which was actually added (either { @code element} or its copy). * @throws IncorrectOperationException if the modification is not supported or not possible for some reason. */ public PsiElement addAfter(@NotNull PsiElement element, PsiElement anchor) { throw new IncorrectOperationException(); } /** * Checks if it is possible to add the specified element as a child to this element, and throws an exception if the * add is not possible. Does not actually modify anything. * * @param element the child element to check the add possibility. * @throws IncorrectOperationException if the modification is not supported or not possible for some reason. * @deprecated not all PSI implementations implement this method correctly. */ @Deprecated public void checkAdd(@NotNull PsiElement element) { throw new IncorrectOperationException(); } /** * Adds a range of elements as children to this PSI element. * * @param first the first child element to add. * @param last the last child element to add (must have the same parent as { @code first}) * @return the first child element which was actually added (either { @code first} or its copy). * @throws IncorrectOperationException if the modification is not supported or not possible for some reason. */ public PsiElement addRange(PsiElement first, PsiElement last) { throw new IncorrectOperationException(); } /** * Adds a range of elements as children to this PSI element, before the specified anchor element. * * @param first the first child element to add. * @param last the last child element to add (must have the same parent as { @code first}) * @param anchor the anchor before which the child element is inserted (must be a child of this PSI element) * @return the first child element which was actually added (either { @code first} or its copy). * @throws IncorrectOperationException if the modification is not supported or not possible for some reason. */ public PsiElement addRangeBefore(@NotNull PsiElement first, @NotNull PsiElement last, PsiElement anchor) { throw new IncorrectOperationException(); } /** * Adds a range of elements as children to this PSI element, after the specified anchor element. * * @param first the first child element to add. * @param last the last child element to add (must have the same parent as { @code first}) * @param anchor the anchor after which the child element is inserted (must be a child of this PSI element) * @return the first child element which was actually added (either { @code first} or its copy). * @throws IncorrectOperationException if the modification is not supported or not possible for some reason. */ public PsiElement addRangeAfter(PsiElement first, PsiElement last, PsiElement anchor) { throw new IncorrectOperationException(); } /** * Deletes this PSI element from the tree. * * @throws IncorrectOperationException if the modification is not supported or not possible for some reason (for * example, the file containing the element is read-only). */ public void delete() { throw new IncorrectOperationException(); } /** * Checks if it is possible to delete the specified element from the tree, and throws an exception if the add is not * possible. Does not actually modify anything. * * @throws IncorrectOperationException if the modification is not supported or not possible for some reason. * @deprecated not all PSI implementations implement this method correctly. */ @Deprecated public void checkDelete() { throw new IncorrectOperationException(); } /** * Deletes a range of children of this PSI element from the tree. * * @param first the first child to delete (must be a child of this PSI element) * @param last the last child to delete (must be a child of this PSI element) * @throws IncorrectOperationException if the modification is not supported or not possible for some reason. */ public void deleteChildRange(PsiElement first, PsiElement last) { throw new IncorrectOperationException(); } /** * Replaces this PSI element (along with all its children) with another element (along with the children). * * @param newElement the element to replace this element with. * @return the element which was actually inserted in the tree (either { @code newElement} or its copy) * @throws IncorrectOperationException if the modification is not supported or not possible for some reason. */ public PsiElement replace(@NotNull PsiElement newElement) { throw new IncorrectOperationException(); } /** * Checks if this PSI element is valid. Valid elements and their hierarchy members can be accessed for reading and * writing. Valid elements can still correspond to underlying documents whose text is different, when those * documents have been changed and not yet committed * ({@link PsiDocumentManager#commitDocument(com.intellij.openapi.editor.Document)}). * (In this case an attempt to change PSI will result in an exception). * <p> * Any access to invalid elements results in {@link PsiInvalidElementAccessException}. * <p> * Once invalid, elements can't become valid again. * <p> * Elements become invalid in following cases: * <ul> * <li>They have been deleted via PSI operation ({@link #delete()})</li> * <li>They have been deleted as a result of an incremental reparse (document commit)</li> * <li>Their containing file has been changed externally, or renamed so that its PSI had to be rebuilt from * scratch</li> * </ul> * * @return true if the element is valid, false otherwise. * @see com.intellij.psi.util.PsiUtilCore#ensureValid(PsiElement) */ public boolean isValid() { return true; } /** * Checks if the contents of the element can be modified (if it belongs to a non-read-only source file.) * * @return true if the element can be modified, false otherwise. */ public boolean isWritable() { return true; } /** * Returns the reference from this PSI element to another PSI element (or elements), if one exists. If the element * has multiple associated references (see {@link #getReferences()} for an example), returns the first associated * reference. * * @return the reference instance, or null if the PSI element does not have any associated references. * @see com.intellij.psi.search.searches.ReferencesSearch */ public PsiReference getReference() { return reference; } /** * Returns all references from this PSI element to other PSI elements. An element can have multiple references when, * for example, the element is a string literal containing multiple sub-strings which are valid full-qualified class * names. If an element contains only one text fragment which acts as a reference but the reference has multiple * possible targets, {@link PsiPolyVariantReference} should be used instead of returning multiple references. * <p> * Actually, it's preferable to call {@link PsiReferenceService#getReferences} instead as it allows adding * references by plugins when the element implements {@link ContributedReferenceHost}. * * @return the array of references, or an empty array if the element has no associated references. * @see PsiReferenceService#getReferences * @see com.intellij.psi.search.searches.ReferencesSearch */ @NotNull public PsiReference[] getReferences() { return new PsiReference[]{reference}; } /** * Passes the declarations contained in this PSI element and its children for processing to the specified scope * processor. * * @param processor the processor receiving the declarations. * @param lastParent the child of this element has been processed during the previous step of the tree up walk * (declarations under this element do not need to be processed again) * @param place the original element from which the tree up walk was initiated. * @return true if the declaration processing should continue or false if it should be stopped. */ public boolean processDeclarations(@NotNull PsiScopeProcessor processor, @NotNull ResolveState state, PsiElement lastParent, @NotNull PsiElement place) { return false; } /** * Returns the element which should be used as the parent of this element in a tree up walk during a resolve * operation. For most elements, this returns {@code getParent()}, but the context can be overridden for some * elements like code fragments. * * @return the resolve context element. */ public PsiElement getContext() { return null; } /** * Checks if an actual source or class file corresponds to the element. Non-physical elements include, for example, * PSI elements created for the watch expressions in the debugger. Non-physical elements do not generate tree change * events. Also, {@link PsiDocumentManager#getDocument(PsiFile)} returns null for non-physical elements. Not to be * confused with {@link FileViewProvider#isPhysical()}. * * @return true if the element is physical, false otherwise. */ public boolean isPhysical() { return true; } /** * Returns the scope in which the declarations for the references in this PSI element are searched. * * @return the resolve scope instance. */ @NotNull public GlobalSearchScope getResolveScope() { return getContainingFile().getResolveScope(); } /** * Returns the scope in which references to this element are searched. * * @return the search scope instance. * @see { @link com.intellij.psi.search.PsiSearchHelper#getUseScope(PsiElement)} */ @NotNull public SearchScope getUseScope() { return getContainingFile().getResolveScope(); } /** * Returns the AST node corresponding to the element. * * @return the AST node instance. */ public ASTNode getNode() { return null; } /** * toString() should never be presented to the user. */ public String toString() { return "Name : " + name + " at offset " + start + " to " + end + " in " + project; } /** * This method shouldn't be called by clients directly, because there are no guarantees of it being symmetric. It's * called by {@link PsiManager#areElementsEquivalent(PsiElement, PsiElement)} internally, which clients should * invoke instead.<p/> * <p> * Implementations of this method should return {@code true} if the parameter is resolve-equivalent to {@code this}, * i.e. it represents the same entity from the language perspective. See also {@link * PsiManager#areElementsEquivalent(PsiElement, PsiElement)} documentation. */ public boolean isEquivalentTo(PsiElement another) { return this == another; } public Icon getIcon(int flags) { return null; } public PsiElement getNameIdentifier() { return this; } public PsiElement setName(@NotNull String name) { this.name = name; return this; } public <T> void putUserData(@NotNull Key<T> key, @Nullable T value) { boolean control = true; while (control) { KeyFMap map = getUserMap(); KeyFMap newMap = (value == null) ? map.minus(key) : map.plus(key, value); if ((newMap.equalsByReference(map)) || changeUserMap(map, newMap)) { control = false; } } } protected boolean changeUserMap(KeyFMap oldMap, KeyFMap newMap) { return updater.compareAndSet(this, oldMap, newMap); } protected KeyFMap getUserMap() { return myUserMap; } public <T> T getCopyableUserData(Key<T> key) { KeyFMap map = getUserData(COPYABLE_USER_MAP_KEY); return (map == null) ? null : map.get(key); } public <T> T getUserData(@NotNull Key<T> key) { T t = getUserMap().get(key); if (t == null && key instanceof KeyWithDefaultValue) { KeyWithDefaultValue<T> key1 = (KeyWithDefaultValue<T>) key; t = putUserDataIfAbsent(key, key1.getDefaultValue()); } return t; } public <T> T putUserDataIfAbsent(Key<T> key, T value) { while (true) { KeyFMap map = getUserMap(); T oldValue = map.get(key); if (oldValue != null) { return oldValue; } KeyFMap newMap = map.plus(key, value); if ((newMap.equalsByReference(map)) || changeUserMap(map, newMap)) { return value; } } } public <T> void putCopyableUserData(Key<T> key, T value) { boolean control = true; while (control) { KeyFMap map = getUserMap(); KeyFMap copyableMap = map.get(COPYABLE_USER_MAP_KEY); if (copyableMap == null) copyableMap = KeyFMap.EMPTY_MAP; KeyFMap newCopyableMap = (value == null) ? copyableMap.minus(key) : copyableMap.plus(key, value); KeyFMap newMap = (newCopyableMap.isEmpty()) ? map.minus(COPYABLE_USER_MAP_KEY) : map.plus(COPYABLE_USER_MAP_KEY, newCopyableMap); if ((newMap.equalsByReference(map)) || changeUserMap(map, newMap)) control = false; } } public <T> boolean replace(Key<T> key, @Nullable T oldValue, @Nullable T newValue) { while (true) { KeyFMap map = getUserMap(); if (map.get(key) != oldValue) { return false; } else { KeyFMap newMap = (newValue == null) ? map.minus(key) : map.plus(key, newValue); if ((newMap == map) || changeUserMap(map, newMap)) { return true; } } } } public void copyCopyableDataTo(UserDataHolderBase clone) { clone.putUserData(COPYABLE_USER_MAP_KEY, getUserData(COPYABLE_USER_MAP_KEY)); } public boolean isUserDataEmpty() { return getUserMap().isEmpty(); } public ItemPresentation getPresentation() { return new ItemPresentation() { public String getPresentableText() { return getName(); } public String getLocationString() { return getContainingFile().getName(); } public Icon getIcon(boolean unused) { return (unused) ? null : null; //iconProvider.getIcon(LSPPsiElement.this) } }; } public String getName() { return name; } public void navigate(boolean requestFocus) { Editor editor = FileUtils.editorFromPsiFile(getContainingFile()); if (editor == null) { OpenFileDescriptor descriptor = new OpenFileDescriptor(getProject(), getContainingFile().getVirtualFile(), getTextOffset()); ApplicationUtils.invokeLater(() -> ApplicationUtils .writeAction(() -> FileEditorManager.getInstance(getProject()).openTextEditor(descriptor, false))); } } /** * Returns the file containing the PSI element. * * @return the file instance, or null if the PSI element is not contained in a file (for example, the element * represents a package or directory). * @throws PsiInvalidElementAccessException if this element is invalid */ public PsiFile getContainingFile() { return file; } /** * Returns the project to which the PSI element belongs. * * @return the project instance. * @throws PsiInvalidElementAccessException if this element is invalid */ @NotNull public Project getProject() { return project; } /** * Returns the offset in the file to which the caret should be placed when performing the navigation to the element. * (For classes implementing {@link PsiNamedElement}, this should return the offset in the file of the name * identifier.) * * @return the offset of the PSI element. */ public int getTextOffset() { return start; } public boolean canNavigateToSource() { return true; } public boolean canNavigate() { return true; } protected void clearUserData() { setUserMap(KeyFMap.EMPTY_MAP); } protected void setUserMap(KeyFMap map) { myUserMap = map; } }