PsiTreeChangeListener code tracking

已回答

Hi!

I want to track file modifications in form of code e.g field declarations, methods and so on in a project. Also I want to get both old and new versions of the java files when a modification is detected.

For this purpose I know that I should use the PsiTreeChangeListener however I should avoid processing the PsiTreeChangeEvent and instead use CachedValue and PsiModificationTracker.MODIFICATION_COUNT.

I already made a similar post:

https://intellij-support.jetbrains.com/hc/en-us/community/posts/360007885220/

My questions are:
How do I use CachedValue and PsiModificationTracker in the Listener (initialization)?
How to check whether files were modificated?

Any code examples/snippets are welcome
Thanks!

 

1

Are you referring to javadoc from com.intellij.psi.PsiTreeChangeEvent regarding "instead use CachedValue and PsiModificationTracker.MODIFICATION_COUNT"?

What scope of changes do you want to track? Just some specific files? Whole project? For what reason?

Please explain your use case in full detail.

0

Hi Yann, thank you for your reply!

Are you referring to javadoc from com.intellij.psi.PsiTreeChangeEvent regarding "instead use CachedValue and PsiModificationTracker.MODIFICATION_COUNT"?

Yes, I am.

Use case description:

My goal is to write an Intellij plugin for an existing tool. The tool performs source code analyzes. This tool requires two versions of the entire source code of a Java project as input. An "old" version (before a change was made in the source code) and a "new" version (after a change was made). The changes are all about changes of any kind in Java files (Java elements such as variables, methods, etc.) So every time a change is made in a Java class (project level), the entire source code of the project should be output / saved before the change and after the change.

I hope I have been able to explain the use case in a reasonably understandable way.

0

Thank you for your answer!

I have registered a VFS Listener according to the documentation but somehow I receive the same output on both methods before and after.The events also seem to fire only when I press "ctrl + S" and not when I change some code in the editor. My code is below. Am I missing something?

Plugins.xml:

<applicationListeners>
<listener class="VfsChangeListener" topic="com.intellij.openapi.vfs.newvfs.BulkFileListener"/>
</applicationListeners>
public class VfsChangeListener implements BulkFileListener {

@Override
public void before(@NotNull List<? extends VFileEvent> events) {
handleEvents(events);
}

@Override
public void after(@NotNull List<? extends VFileEvent> events) {
handleEvents(events);
}

private Project getActiveProject(){
Project[] projects = ProjectManager.getInstance().getOpenProjects();
Project activeProject = null;
for (Project project : projects) {
Window window = WindowManager.getInstance().suggestParentWindow(project);
if (window != null && window.isActive()) {
activeProject = project;
}
}
return activeProject;
}

private boolean isPsiFileInProject(Project project, PsiFile psiFile) {
boolean inProject = ProjectRootManager.getInstance(project)
.getFileIndex().isInContent(psiFile.getVirtualFile());
if (!inProject) {
System.out.println("File " + psiFile + " not in current project " + project);
}
return inProject;
}

private void iterateContent(Project project) {
ProjectFileIndex.SERVICE.getInstance(project).iterateContent(fileOrDir -> {
PsiFile sourceFile = PsiManager.getInstance(project).findFile(fileOrDir);
if (sourceFile instanceof PsiJavaFile) {
PsiClass[] classes = ((PsiJavaFile) sourceFile).getClasses();
for (PsiClass sc : classes) {
System.out.println(sc.getText());
}
}
return true;
});
}

private void handleEvents(@NotNull List<? extends VFileEvent> events) {
Project project = getActiveProject();
for(VFileEvent event : events){
VirtualFile file = event.getFile();
if(file!=null){
PsiFile psiFile = PsiManager.getInstance(project).findFile(file);
if(psiFile != null && isPsiFileInProject(project,psiFile)){
iterateContent(project);
}
}
}
}
}
0

Thanks for your response Yann!

I already did. I know that I can access the old contents of files in the before event and new content in the after event. My problem remains the same as posted above. I am sorry but can you be more specific? Are you indicating that I should use refresh operations to achieve my goal and if so how do I use them correctly in my BulkFileListener implementation class?

0

You'll probably need both PsiTreeChangeListener (to save Document on change event) and BulkFileListener (to proceed actual change). See similar issue and workaround: https://youtrack.jetbrains.com/issue/IDEA-239773#focus=streamItem-27-4124439.0-0

0

The solution proposed by Artsiom should work, but might be problematic performance-wise. Unfortunately, there is no clear better way of doing this.

0

Thank you guys for your answers!

Artsiom Chapialiou thank you for the workaround!

So now everytime i save a Document on my change event the before and after method of my BulkFileListener are fired. But they both still output the same result and not the "old" and "new" content. Any idea why? The code is above.

My TreeChangeAdapter:

@Override
public void childrenChanged(@NotNull PsiTreeChangeEvent event) {
final PsiFile psiFile = event.getFile();
if (psiFile == null) return;
int errorCount = 0;
if (PsiTreeUtil.hasErrorElements(psiFile)) {
errorCount++;
}
if (errorCount == 0) {
final VirtualFile virtualFile = psiFile.getVirtualFile();
if (virtualFile != null && Objects.equals(virtualFile.getExtension(), "java")) {
final Document document = psiFile.getViewProvider().getDocument();
if (document != null) {
FileDocumentManager.getInstance().saveDocument(document);
}
}
}
}
0

1) PsiTreeUtil.hasErrorElements is really costly and will most likely lead to massive performance decrease, especially if called so frequently like in this place

2) "But they both still output the same result and not the "old" and "new" content" please be specific, what code/method do you call?

0

1) Ok thanks but is there another way to do this efficiently? Should I call it from ApplicationManager.runReadAction?

2) So I'm calling the handleEvents method in both before and after method. The handleEvents method calls iterateContent method which basically iterates over the project files and outputs the contents of java files for the moment. So I want the contents of the java files before a change happened and after the change.

0

1) Please explain the use case. This returns only subset of actual errors in the file, and counting the total of errors while the plugin was running seems like a strange metric?

2) If you want to access before/content of actual text, you'll need to use Document and com.intellij.openapi.editor.event.BulkAwareDocumentListener.Simple.

0

1) I just want to save the Document only if there are no errors e.g it is a valid java file. So I thought I would use a counter and increment it if hasErrorElements returns true. And if the counter is zero the file would be ok at least so I thought. I didn't know that PsiTreeUtil.hasErrorElements is so costly.

2) instead of using BulkFileListener I should switch to BulkAwareDocumentListener?

0

1) if you define "valid java file" as "IntelliJ's Java parser has found no syntax level error" then yes

2) yes

 

0

1) Let me try to explain this using an example: Let's say the use types private int age; in the editor. This would be valid. but if the user types only private or "pr" this would be invalid and in this case I wouldn't want to save the document. I hope this is somehow understandable.

2) I added the BulkAwareDocumentListener in the childrenChanged Method of my PsiTreeChangeAdapter using document.addDocumentListener. But unlike in the BulkFileListener the beforeDocumentChangeNonBulk and afterDocumentChangeNonBulk fire everytime I type something in the editor and not on Document save. I want to get the contents of all the java files of a project before and after a changed happened in a document.

0

1) I see, misread your code

2) use EditorFactory.getInstance().getEventMulticaster().addDocumentListener(Listener, Disposable). you can use com.intellij.openapi.fileEditor.FileDocumentManagerListener to be notified when save is invoked

0

Just to be sure. Using what you suggested in 2) will I be able to get the contents of files before and after they were changed and all other file contents that weren't changed (project level)? My problem is that BulkAwareDocumentListener.Simple fires everytime something is typed in the editor and not when the Document is saved in my PsiTreeChangeAdapter.childrenChanged Method. You suggested using FileDocumentManagerListener to do this but I just don't know how.

Is there a way to check for error count in the analysis completed popup (image below)? EDIT: I used WolfTheProblemSolverImpl#hasSyntaxErrors for this.

Sorry for the many questions.

0

Sorry for delay, please use com.intellij.AppTopics#FILE_DOCUMENT_SYNC to register FileDocumentManagerListener.

0

Yann, could you please give a clue starting what Idea version BulkAwareDocumentListener is added?

It's not presented at 192

0

Looking into commit history it was added at https://github.com/JetBrains/intellij-community/commit/5c03fd94b29e3ff99ffdb2064a6e8d738502c257

And the earliest branch with this commit is: idea/193.2956.37

So for 192 DocumentBulkUpdateListener should be used, correct?

Also is there any analogue for bulk Psi changes? I.e. PsiTreeChangeListener -> PsiTreeBulkChangeListener 

0

com.intellij.openapi.editor.ex.DocumentBulkUpdateListener has been deprecated after (193) so use com.intellij.openapi.editor.event.DocumentListener directly

 

not aware of something similar to com.intellij.psi.PsiTreeChangeListener (which no one should be using anyway :))

0

Thanks for your response Yann!

I tried what you suggested but it doesn't work. Any idea why?

Plugins.xml

<applicationListeners>
<listener class="MyDocumentManagerListener" topic="com.intellij.AppTopics$FILE_DOCUMENT_SYNC" />
</applicationListeners>

MyDocumentManagerListener:

public class MyDocumentManagerListener implements FileDocumentManagerListener {


@Override
public void beforeDocumentSaving(@NotNull Document document) {
System.out.println("saving event");
}
}
0

Sorry my bad. This works now:

<applicationListeners>
<listener class="MyDocumentManagerListener" topic="com.intellij.openapi.fileEditor.FileDocumentManagerListener" />
</applicationListeners>

But it does the same job like:

<extensions defaultExtensionNs="com.intellij">
<!-- Add your extensions here -->>
<fileDocumentManagerListener implementation="MyDocumentManagerListener"/>
</extensions>

Is there any difference? And also I still have the same problem described in this comment: 

https://intellij-support.jetbrains.com/hc/en-us/community/posts/360008112119/comments/360001655819 

Sorry that I don't get what you are trying to explain.

0

Yes, registration via <listener> is just different way. What exactly is "same problem"?

0

请先登录再写评论。