Skip to content

PopupCloser is not always properly uninstalled and pollutes the filterTable of the Display #3582

@FlorianKroiss

Description

@FlorianKroiss

This bug originally surfaced in LSP4E, see eclipse-lsp4e/lsp4e#1448
I managed to trace it back to Platform.UI. So this is essentially a repost with a hopefully better description and a self-contained example.

Steps to reproduce:

  1. Run the given example below
  2. Click into the text field hold down the dot key.
  3. After every entered dot, the completions popup is closed and reopened
  4. Hover over the popup with the additional information
  5. See that this popup is recreated rapidly while also blocking the UI thread. This only gets worse the longer the program is running.

Bug in Action:
https://github.com/user-attachments/assets/0d5484d9-b7ee-41d3-931a-44b5a7119142

Every time the completions popup is shown, a PopupCloser is added as a filter on the current Display, which listens for various mouse events and tries to replace the detail popup, once the mouse hover over it. However, this PopupCloser is not properly removed when the completions popup is closed. Since the completions popup is closed and reopenend after every typed character, we end up with a massive amount of Filters on the Display. As soon as the mouse enters the detail popup, every added filter is triggered and reopens the popup. This only gets worse, the more characters are typed.

Filter Table of the Display after a while. Most of the other filters not visible are also PopupCloser:
Image

The code for handling Popups looks quite complicated to me, but two locations look especially suspicious to me.
When the completions are updated, the shell is directly disposed. I think hide() should be called here?

@Override
List<ICompletionProposal> computeProposals(int offset) {
if (fProposalShell != null) {
fProposalShell.dispose();
}
showProposals(true);
return fComputedProposals;
}

The uninstallation of the PopupCloser is only called if the popup shell is still valid. But if the shell was disposed any other way (see above code), then the PopupCloser has no way to cleanup the filters that it added to the Display. I think uninstall should always be called, because the PopupCloser has its own checks, if the Shell or the Display are still valid:

if (isValid(fProposalShell)) {
fContentAssistant.removeContentAssistListener(this, ContentAssistant.PROPOSAL_SELECTOR);
fPopupCloser.uninstall();
fProposalShell.setVisible(false);
fProposalShell.dispose();
fProposalShell= null;
}

Self contained example:

import org.eclipse.jface.layout.GridDataFactory;
import org.eclipse.jface.text.Activator;
import org.eclipse.jface.text.DefaultInformationControl;
import org.eclipse.jface.text.Document;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IInformationControl;
import org.eclipse.jface.text.IInformationControlCreator;
import org.eclipse.jface.text.ITextViewer;
import org.eclipse.jface.text.contentassist.CompletionProposal;
import org.eclipse.jface.text.contentassist.ContentAssistant;
import org.eclipse.jface.text.contentassist.ContextInformation;
import org.eclipse.jface.text.contentassist.ICompletionProposal;
import org.eclipse.jface.text.contentassist.IContentAssistProcessor;
import org.eclipse.jface.text.contentassist.IContentAssistant;
import org.eclipse.jface.text.contentassist.IContextInformation;
import org.eclipse.jface.text.contentassist.IContextInformationValidator;
import org.eclipse.jface.text.source.ISourceViewer;
import org.eclipse.jface.text.source.SourceViewer;
import org.eclipse.jface.text.source.SourceViewerConfiguration;
import org.eclipse.swt.SWT;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Shell;


public class MinimalSWT {
	
    private static final class ContentAssistProcessor implements IContentAssistProcessor {
		@Override
		public String getErrorMessage() {
			return null;
		}

		@Override
		public IContextInformationValidator getContextInformationValidator() {
			return null;
		}

		@Override
		public char[] getContextInformationAutoActivationCharacters() {
			return null;
		}

		@Override
		public char[] getCompletionProposalAutoActivationCharacters() {
			return new char[] {'.'};
		}

		@Override
		public IContextInformation[] computeContextInformation(ITextViewer viewer, int offset) {
			// TODO Auto-generated method stub
			return null;
		}

		@Override
		public ICompletionProposal[] computeCompletionProposals(ITextViewer viewer, int offset) {
			var ctx = new ContextInformation("foo", "bar");
			
			var comp = new CompletionProposal("foo", 0, 0, 0, null, "foo", ctx, "additional");
			return new ICompletionProposal[] {comp};
		}
	}

	public static void main(String[] args) {
        Display display = new Display();
        Shell shell = new Shell(display);
        shell.setLayout(new GridLayout(1, false));
        shell.setText("FilterTable Pollution");
        shell.setSize(300, 200);
        
        // Not sure how this is supposed to be activated when running plain Java
        // So just initialize it directly.
        Activator a = new Activator();
        a.start(null);

        SourceViewer viewer = new SourceViewer(shell, null, SWT.V_SCROLL | SWT.H_SCROLL | SWT.BORDER);
        GridDataFactory.fillDefaults().grab(true, true).applyTo(viewer.getControl());
        
        Document doc = new Document();
        viewer.setDocument(doc);
        
        ContentAssistant assistant = new ContentAssistant(true);
        ContentAssistProcessor processor = new ContentAssistProcessor();
		assistant.setContentAssistProcessor(processor, IDocument.DEFAULT_CONTENT_TYPE);
		assistant.enableAutoActivation(true);
        assistant.setAutoActivationDelay(10);
        assistant.setProposalPopupOrientation(IContentAssistant.PROPOSAL_OVERLAY);
        assistant.setContextInformationPopupOrientation(IContentAssistant.CONTEXT_INFO_ABOVE);
        
        assistant.setInformationControlCreator(new IInformationControlCreator() {
            @Override
            public IInformationControl createInformationControl(Shell parent) {
                return new DefaultInformationControl(parent, true);
            }
        });
        
        viewer.configure(new SourceViewerConfiguration() {
        	@Override
        	public IContentAssistant getContentAssistant(ISourceViewer sourceViewer) {
        		return assistant;
        	}
        });
        
        shell.open(); 

        while (!shell.isDisposed()) {
            if (!display.readAndDispatch()) {
                display.sleep();
            }
        }
        display.dispose();
    }
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions