Creating separate resolve scope for specific files

Answered

My plugin provides support for Clojure files. A project which is gaining popularity recently is Babashka, which uses Clojure for shell scripting. Babashka provides an executable with a lot of built-in libraries, but I'm not sure how to support this.

The basic problem is that there is nothing distinguishing Babashka files from Clojure files, and they tend to be mixed into the same project in arbitrary locations. My plan is to allow the user to specify which files are Babashka files, and then symbol resolution within those files will have to take into account the extra dependencies and other semantic elements available to Babashka scripts by default.

I've added a Babashka facet, and from a user-specified path to the executable I can get the required dependencies and download them using Maven. But I'm not sure how to attach them to the project. If I attach them to the module as libraries, then they will be available to normal Clojure code, which is incorrect. I need a way to be able to create a completely separate resolve scope, almost like a separate module. I considered using a separate Babashka module, but since the Babashka scripts are often intermingled in with the Clojure files managing the content roots would be difficult. Could I do this by using a separate module with no content roots to manage the Babashka dependencies, and then use that module's resolve scope when resolving some files in the original module using ResolveScopeProvider or something similar?

Any suggestions for the best way to handle this?

0
8 comments

It's possible to add individual files as content roots, but this may be inconvenient. You may try adding these additional dependencies via ResolveScopeEnlarger extension point instead.

0

Thanks Nikolay. I finally got around to implementing this. In the end what I have done is:

  1. I'm no longer using a facet - the user configures Babashka project-wide. When they do so, I figure out the deps and download them, and attach them to the project as libraries.
  2. I've created an action for users to be able to mark specific files as Babashka files, and the marking is stored using a PerFileMappings.
  3. I've implemented a ResolveScopeProvider which, when the VirtualFile in question is marked as being a Babashka script, finds the relevant Project-level library dependencies and returns a scope only containing the Babashka ones. This is because the Babashka scripts should only see the Babashka deps, and everything else should not see those at all.

However, this doesn't work - no symbols resolve, and I think it's because the project libraries are not added to a module so they're not indexed. I can imagine a couple of possible solutions:

  1. Create a fake Babashka module which just contains its dependencies but is otherwise unused. This will be visible to the user and is liable to be confusing.
  2. Add the deps to all modules with files marked as being Babashka scripts, and then in my ResolveScopeProvider either return the Babashka deps as above or a scope similar to Module-with-library-deps but with the Babashka deps filtered out (so nothing else sees them) for other files. This seems brittle, though. If this is the best solution, I'll probably go back to using a facet.
  3. Perhaps force the indexing of the Babashka deps even though they're not attached to modules using an IndexableSetContributor? This also feels brittle, and liable to be confusing since they'll still be marked as unused in Project Structure.

Any advice on the best solution here?

0

Yes, libraries which aren't added to dependencies of some module aren't indexed indeed. You may try using com.intellij.openapi.roots.AdditionalLibraryRootsProvider to create a synthetic library instead. It won't be shown in Project Structure dialog and user won't be able to delete or edit it, but it may be shown under 'External Libraries' node in Project View and its files will be indexed.

0

Thanks Nikolay, that looks useful. I have a couple of questions about that:

  1. According to the doc, SyntheticLibrary roots will be added to allScope for a project. Is there a way to prevent this? Otherwise my standard Clojure code will see symbols from these libs.
  2. One thing I can't immediately see is how to get a collection of all the SyntheticLibrary instances for a project in order to create a scope.
  3. If the user updates the config which affects these libraries, how do I trigger a recalculation of them?
0

Hi Colin,

Please see answers below.

1. According to the doc, SyntheticLibrary roots will be added to allScope for a project. Is there a way to prevent this? Otherwise my standard Clojure code will see symbols from these libs.

Provided SyntheticLibrary instances are automatically added to `GlobalSearchScope.allScope(project)`. This scope is used in common places like "Navigate | File..." popup. However, you can build a proper resolve scope when resolving symbols from Closure files. Something like you've already done before:

I've implemented a ResolveScopeProvider which, when the VirtualFile in question is marked as being a Babashka script, finds the relevant Project-level library dependencies and returns a scope only containing the Babashka ones.

Will it work?


2. One thing I can't immediately see is how to get a collection of all the SyntheticLibrary instances for a project in order to create a scope.

Currently, IDE doesn't keep list of all SyntheticLibraries, so you can go ahead and fetch them:

Collection<SyntheticLibrary> projectLibraries = AdditionalLibraryRootsProvider.EP_NAME.extensions()
.flatMap(el -> el.getAdditionalProjectLibraries(getProject()).stream())
.collect(Collectors.toList());

 

3. If the user updates the config which affects these libraries, how do I trigger a recalculation of them?


The old way was to fire `rootsChanged` event:

ProjectRootManagerEx.getInstanceEx(myProject).makeRootsChange(EmptyRunnable.getInstance(), false, true)

 

However, a new way is to use `com.intellij.openapi.roots.AdditionalLibraryRootsListener#fireAdditionalLibraryChanged`.

0

Thanks Sergey. I agree that the allScope thing is probably ok. I am now creating a SyntheticLibrary which I can see in the project view. I'm also returning a scope from my ResolveScopeProvider which I can see being called and is switching correctly based on the file being marked.

However the scope I'm using isn't returning anything from my index searches. The scope I return is based on the list of jars in the SyntheticLibrary, and I created a scope extending LibraryScopeBase and passed it the list of VirtualFiles as classes in the constructor. As far as I can tell the jars from the SyntheticLibrary are never being indexed, and the files from those jars are never being passed to my custom Scope - I only see the files from my project libraries which are added as module dependencies. I'm actually not sure whether LibraryScopeBase is appropriate, since it has the following:

protected VirtualFile getFileRoot(@NotNull VirtualFile file) {
if (myIndex.isInLibraryClasses(file)) {
return myIndex.getClassRootForFile(file);
}
if (myIndex.isInLibrarySource(file)) {
return myIndex.getSourceRootForFile(file);
}
return null;
}

and I don't know whether those index checks will return true for files in SyntheticLibraries or not. In the project view, I can see the correct list of jars, but only the two jars which also appear as module dependencies are able to be expanded (which I believe means that they're actually in use) - see below. I thought that perhaps the files weren't being indexed because the indexing happens lazily on demand and the scope wasn't correct, but I never see my scope being called for files in those jars at all. Can you think why this might be happening?

0

Indexing shouldn't depend on resolve scopes. What files are being added to SyntheticLibrary roots? What does `file.getPath()` returns for the library roots? Is it file://... or jar://...? If it's file://..., the content of such zip/jar files won't be traversed by indexer. You can convert it to jar:// with

JarFileSystem.getInstance().findLocalVirtualFileByPath(root)

If jar://... files are passed as roots to new SynthticLibrary instance, these files should be indexed. You can verify if a file inside jar/zip is indexed in the following way: open "Navigate | File..." and type there the file name. For example, here is how it works for Node.js Yarn integration:

0

Thanks Sergey, that was indeed the problem. I have this all working now, it's fairly complex with a fair number of moving parts, but it seems to be working well. Thanks for the help!

0

Please sign in to leave a comment.