DocumentationProvider for fake PsiElement

Answered

Hi. I want to use DocumentationProvider for all tokens in the text document. So I create some fake PsiElements for this tokens. But the quick documentation popup is displayed in the correct place only if I position the editor cursor on the token and press Ctrl + Q. When I hover over symbols with the mouse, the popup window is displayed either at the beginning of the document or in the middle after scrolling. How can I put the quick documentation popup on mouse hover in the right place?

0
5 comments

Hi,

Please provide more information:

  • DocumentationProvider implementation
  • fake PsiElement implementations
0
class MyDocumentationProvider extends AbstractDocumentationProvider {
override def getCustomDocumentationElement(
editor: Editor,
file: PsiFile,
context: PsiElement,
targetOffset: Int
): PsiElement =
EditorEventManager.forEditor(editor) match {
case Some(eventManager) => eventManager.getElementAtOffset(targetOffset)
case None => null
}

override def generateDoc(
element: PsiElement,
originalElement: PsiElement
): String =
getQuickNavigateInfo(element, originalElement)

override def getQuickNavigateInfo(
element: PsiElement,
originalElement: PsiElement
): String =
element match {
case myPsiElement: MyPsiElement =>
EditorEventManager
.forUri(
FileUtils.VFSToURI(myPsiElement.getContainingFile.getVirtualFile)
)
.fold("")(eventManager =>
eventManager
.requestDoc(eventManager.editor, myPsiElement.getTextOffset)
)
case psiFile: PsiFile =>
val editor = FileUtils.editorFromPsiFile(psiFile)
EditorEventManager
.forEditor(editor)
.fold("")(
_.requestDoc(editor, editor.getCaretModel.getCurrentCaret.getOffset)
)
case _ => ""
}
}

case class MyPsiElement(
var name: String,
project: Project,
start: Int,
end: Int,
psiFile: PsiFile,
editor: Editor
) extends PsiNameIdentifierOwner
with NavigatablePsiElement {
private val COPYABLE_USER_MAP_KEY: Key[KeyFMap] =
Key.create("COPYABLE_USER_MAP_KEY")
private val updater =
AtomicFieldUpdater.forFieldOfType(classOf[MyPsiElement], classOf[KeyFMap])
private val manager = PsiManager.getInstance(project)
private val reference = MyPsiReference(this)
@volatile private val myUserMap: KeyFMap = KeyFMap.EMPTY_MAP

override def getLanguage: Language = PlainTextLanguage.INSTANCE

override def getManager: PsiManager = manager

override def getChildren: Array[PsiElement] = null

override def getParent: PsiElement = getContainingFile

override def getFirstChild: PsiElement = null

override def getLastChild: PsiElement = null

override def getNextSibling: PsiElement = null

override def getPrevSibling: PsiElement = null

override def getTextRange: TextRange =
new TextRange(start, end)

override def getStartOffsetInParent: Int = start

override def getTextLength: Int = end - start

override def findElementAt(offset: Int): PsiElement = null

override def findReferenceAt(offset: Int): PsiReference = null

override def textToCharArray: Array[Char] = name.toCharArray

override def getNavigationElement: PsiElement = this

override def getOriginalElement: PsiElement = null

override def textMatches(sequence: CharSequence): Boolean =
getText == sequence

override def getText: String = name

override def textMatches(element: PsiElement): Boolean =
getText == element.getText

override def textContains(char: Char): Boolean =
getText.contains(char)

override def accept(visitor: PsiElementVisitor): Unit =
visitor.visitElement(this)

override def acceptChildren(visitor: PsiElementVisitor): Unit = {}

override def copy: PsiElement = null

@throws[IncorrectOperationException]
override def add(element: PsiElement): PsiElement =
throw new IncorrectOperationException()

@throws[IncorrectOperationException]
override def addBefore(element: PsiElement, anchor: PsiElement): PsiElement =
throw new IncorrectOperationException()

@throws[IncorrectOperationException]
override def addAfter(element: PsiElement, anchor: PsiElement): PsiElement =
throw new IncorrectOperationException()

@throws[IncorrectOperationException]
override def checkAdd(element: PsiElement): Unit =
throw new IncorrectOperationException()

@throws[IncorrectOperationException]
override def addRange(first: PsiElement, last: PsiElement): PsiElement =
throw new IncorrectOperationException()

@throws[IncorrectOperationException]
override def addRangeBefore(
first: PsiElement,
last: PsiElement,
anchor: PsiElement
): PsiElement =
throw new IncorrectOperationException()

@throws[IncorrectOperationException]
override def addRangeAfter(
first: PsiElement,
last: PsiElement,
anchor: PsiElement
): PsiElement =
throw new IncorrectOperationException()

@throws[IncorrectOperationException]
override def delete(): Unit =
throw new IncorrectOperationException()

@throws[IncorrectOperationException]
override def checkDelete(): Unit =
throw new IncorrectOperationException()

@throws[IncorrectOperationException]
override def deleteChildRange(first: PsiElement, last: PsiElement): Unit =
throw new IncorrectOperationException()

@throws[IncorrectOperationException]
override def replace(newElement: PsiElement): PsiElement =
throw new IncorrectOperationException()

override def isValid: Boolean = true

override def isWritable: Boolean = true

override def getReference: PsiReference = reference

override def getReferences: Array[PsiReference] =
Array(reference)

override def processDeclarations(
processor: PsiScopeProcessor,
state: ResolveState,
lastParent: PsiElement,
place: PsiElement
): Boolean = false

override def getContext: PsiElement = null

override def isPhysical: Boolean = true

override def getResolveScope: GlobalSearchScope =
getContainingFile.getResolveScope

override def getUseScope: SearchScope = getContainingFile.getResolveScope

override def getNode: ASTNode = null

override def toString: String =
s"Name: $name at offset $start to $end in $project"

override def isEquivalentTo(another: PsiElement): Boolean = this == another

override def getIcon(flags: Int): Icon = null

override def getNameIdentifier: PsiElement = this

override def setName(newName: String): PsiElement = {
this.name = newName
this
}

override def putUserData[T](key: Key[T], @Nullable value: T): Unit = {
var control = true
while (control) {
val map = getUserMap
val newMap = if (value == null) map.minus(key) else map.plus(key, value)
if ((newMap eq map) || changeUserMap(map, newMap)) control = false
}
}

override def getCopyableUserData[T](key: Key[T]): T = {
val keyFMap = getUserData(COPYABLE_USER_MAP_KEY)
if (keyFMap == null) null.asInstanceOf[T] else keyFMap.get(key)
}

override def getUserData[T](key: Key[T]): T = {
var t = getUserMap.get(key)
if (t == null && key.isInstanceOf[KeyWithDefaultValue[_]])
t = putUserDataIfAbsent(
key,
key.asInstanceOf[KeyWithDefaultValue[T]].getDefaultValue
)
t
}

private def putUserDataIfAbsent[T](key: Key[T], value: T): T = {
while (true) {
val keyFMap = getUserMap
val oldValue = keyFMap.get(key)
if (oldValue != null) return oldValue
val newMap = keyFMap.plus(key, value)
if ((newMap eq keyFMap) || changeUserMap(keyFMap, newMap)) return value
}
null.asInstanceOf[T]
}

override def putCopyableUserData[T](key: Key[T], value: T): Unit = {
var control = true
while (control) {
val keyFMap = getUserMap
var copyableMap = keyFMap.get(COPYABLE_USER_MAP_KEY)
if (copyableMap == null) copyableMap = KeyFMap.EMPTY_MAP
val newCopyableMap =
if (value == null) copyableMap.minus(key)
else copyableMap.plus(key, value)
val newMap =
if (newCopyableMap.isEmpty) keyFMap.minus(COPYABLE_USER_MAP_KEY)
else keyFMap.plus(COPYABLE_USER_MAP_KEY, newCopyableMap)
if ((newMap eq keyFMap) || changeUserMap(keyFMap, newMap)) control = false
}
}

protected def changeUserMap(oldMap: KeyFMap, newMap: KeyFMap): Boolean =
updater.compareAndSet(this, oldMap, newMap)

protected def getUserMap: KeyFMap = myUserMap

override def getPresentation: ItemPresentation = new ItemPresentation {
override def getPresentableText: String = getName

override def getLocationString: String = getContainingFile.getName

override def getIcon(unused: Boolean): Icon = null
}

override def getName: String = name

override def navigate(requestFocus: Boolean): Unit = {
if (FileUtils.editorFromPsiFile(getContainingFile) == null) {
val fileDescriptor =
new OpenFileDescriptor(
getProject,
getContainingFile.getVirtualFile,
getTextOffset
)
ApplicationUtils.invokeLater(() =>
ApplicationUtils.writeAction(() =>
FileEditorManager
.getInstance(getProject)
.openTextEditor(fileDescriptor, false)
)
)
}
}

override def getContainingFile: PsiFile = psiFile

override def getProject: Project = project

override def getTextOffset: Int = start

override def canNavigateToSource: Boolean = true

override def canNavigate: Boolean = true

protected def setUserMap(map: KeyFMap): Unit =
myUserMap = map
}

case class MyPsiReference(var element: PsiElement) extends PsiReference {
def getElement: PsiElement = element

def getRangeInElement: TextRange = new TextRange(0, element.getTextLength)

def resolve: PsiElement = element

def getCanonicalText: String = element.getText

@throws[IncorrectOperationException]
def handleElementRename(newElementName: String): PsiElement = element

@throws[IncorrectOperationException]
def bindToElement(newElement: PsiElement): PsiElement = {
this.element = newElement
newElement
}

def isReferenceTo(newElement: PsiElement): Boolean =
this.element == newElement

override def getVariants: Array[AnyRef] = Array()

def isSoft: Boolean = false
}
0

Hi,
1. What is EditorEventManager.forEditor(editor)? What element is returned from MyDocumentationProvider.getCustomDocumentationElement() when you observe an issue?
2. Why do you implement your own fake element instead of using FakePsiElement? Is there any additional functionality that you implement? If not, I suggest switching to FakePsiElement to avoid implementing redundant code (it may help to spot the issue cause).

0
A possible solution is to implement InjectedLanguageManager interface and specifically override findInjectedElementAt function. And now it works.
<projectService serviceInterface="com.intellij.lang.injection.InjectedLanguageManager"
serviceImplementation="MyInjectedLanguageManagerImpl" overrides="true" />
override def findInjectedElementAt(
hostFile: PsiFile,
hostDocumentOffset: Int
): PsiElement =
EditorEventManager.forEditor(FileUtils.editorFromPsiFile(hostFile)) match {
case Some(eventManager) =>
eventManager.getElementAtOffset(hostDocumentOffset)
case None => instance.findInjectedElementAt(hostFile, hostDocumentOffset)
}

 

0

No, that's a workaround. One should never override services provided by the platform in 3rd party plugins.

0

Please sign in to leave a comment.