-
Notifications
You must be signed in to change notification settings - Fork 230
Description
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:
- Run the given example below
- Click into the text field hold down the dot key.
- After every entered dot, the completions popup is closed and reopened
- Hover over the popup with the additional information
- 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:

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?
Lines 333 to 340 in e15e9bf
| @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:
Lines 1112 to 1120 in e15e9bf
| 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();
}
}