Caching project-wide PSI elements by language

Answered

I have functionality in my plugin (xquery-intellij-plugin) to support MarkLogic search options XML configuration from within XQuery that references modules and functions in that or other XQuery files. This allows a user to navigate to the module/function from the XML element referencing it. The relevant code is:

The problem I'm experiencing is that when switching from project A to B and back to A, IntelliJ gets stuck on the "Analyzing ..." action, and updates to the highlighting for the file are not being applied. I'm trying to locate and resolve these issues. One of which I think I've identified as detailed below ...

As part of the logic to support the search options, I have a helper function to list all instances of the options XML in any XQuery file in the current project. This is so I can search that XML for XML elements that reference XQuery functions. The logic:
1. uses `ProjectRootManager.getInstance(project).fileIndex` to enumerate the files in the project;
2. uses `PsiManager.getInstance(project).findFile(file)` to get the XQuery file to locate the options XML elements in.

In order to improve performance, this logic is using `CachedValuesManager.getManager(project).getCachedValue` to cache the value and `PsiModificationTracker.SERVICE.getInstance(project).forLanguage(XQuery)` to invalidate the cache when XQuery file PSI content changes.

The code is:

    private fun getModificationTracker(project: Project): ModificationTracker {
        return PsiModificationTracker.SERVICE.getInstance(project).forLanguages {
            it === XQuery
        }
    }

   private fun getSearchOptions(project: Project): List<PsiElement> {
        return CachedValuesManager.getManager(project).getCachedValue(this, OPTIONS, {
            val options = ArrayList<PsiElement>()
            ProjectRootManager.getInstance(project).fileIndex.iterateContent {
                when (val file = it.toPsiFile(project)) {
                    is XQueryModule -> processOptions(file, options)
                    else -> {
                    }
                }
                true
            }
            CachedValueProvider.Result.create(options, getModificationTracker(project))
        }, false)
    }

    private fun processOptions(node: PsiElement, options: ArrayList<PsiElement>) {
        when (node) {
            is XdmElementNode -> when (node.namespaceUri) {
                NAMESPACE -> options.add(node)
                else -> {
                }
            }
            else -> node.children().forEach { child -> processOptions(child, options) }
        }
    }

In this case, what is the best way to implement this? -- i.e. to have a project global cache of elements from one or more file types that is correctly invalidated when the content of those files change.

I've had a look at PsiTreeChangeListener, but that has a lot of warnings and caveats around its usage and accuracy w.r.t. node changes, accessing lazily evaluatable elements, etc. As such -- and given that I want to scan the entire project -- that does not look like the right approach for this.

4 comments
Comment actions Permalink

Hi Reece,

Could you please provide the first code snippet? It seems to be missing after:

This allows a user to navigate to the module/function from the XML element referencing it. The relevant code is:

0
Comment actions Permalink

Sorry, that was a typo/edit error. I originally linked to https://github.com/rhdunn/xquery-intellij-plugin/blob/a9bf05ee85b932f495302559fab8cc6ce1ca4313/src/plugin-marklogic/main/uk/co/reecedunn/intellij/plugin/marklogic/search/options/SearchOptions.kt#L59, but ended up placing the relevant code inline but forgot I still had that in.

That code should be what is in the "The code is:" part.

0
Comment actions Permalink

Thank you for claryfing.

In general, the approach to use PsiModificationTracker and CachedValuesManager looks fine.

What looks alarming to me is that SearchOptions is an application-level service and is passed as the first argument of the getCachedValue() method. Is it the only reason this service extends UserHolderDataBase? You could pass Project, which is also UserDataHolder. Maybe the code will require some additional adjustments after changing it.

If it doesn't solve the problem, please provide an example project and also add Gradle Wrapper to the project, so we can easily import and run your plugin locally. Currently, it is pretty hard to navigate and understand the code of your plugin because I can't even import it.

0
Comment actions Permalink

Thanks for the reply.

Yes, the service is extending UserDataHolderBase to be used as a cached data holder.

Removing that and passing the project instance to getCachedValue fixes the issue. I've updated the keys to add a prefix to avoid clashes with other keys on the project.

0

Please sign in to leave a comment.