From e1a7cdf8f80a5a35a6cd238333c7700cfe445d70 Mon Sep 17 00:00:00 2001 From: Dung Dong Date: Wed, 14 Oct 2026 21:25:06 +0000 Subject: [PATCH] fix: guard against null ITextEditor document provider ITextEditor.getDocumentProvider() is optional in the Eclipse API and may return null for third party editors. AutoTriggerPartListener dereferenced it unconditionally on every editor activation, so the NPE escaped the IPartListener2 callback and the workbench showed a generic error dialog. Resolve the provider and document defensively in AutoTriggerPartListener (partActivated and onStart) and in QEclipseEditorUtils.getSelectionRange and insertAtCursor, degrading to a no-op instead of throwing. --- .../amazonq/util/AutoTriggerPartListener.java | 30 +++++++++++- .../amazonq/util/QEclipseEditorUtils.java | 31 +++++++++++- .../util/AutoTriggerPartListenerTest.java | 48 +++++++++++++++++++ 3 files changed, 105 insertions(+), 4 deletions(-) diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/util/AutoTriggerPartListener.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/util/AutoTriggerPartListener.java index dcbf7358b..cad325c4d 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/util/AutoTriggerPartListener.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/util/AutoTriggerPartListener.java @@ -81,7 +81,10 @@ private boolean isCustomizedEditorType(final IWorkbenchPart part) { } private void attachDocumentListenerAndUpdateActiveDocument(final ITextEditor editor) { - var document = editor.getDocumentProvider().getDocument(editor.getEditorInput()); + var document = resolveDocument(editor); + if (document == null) { + return; + } attachDocumentListenerAndUpdateActiveDocument(document); } @@ -90,6 +93,29 @@ private void attachDocumentListenerAndUpdateActiveDocument(final IDocument docum setActiveDocument(document); } + /** + * Resolves the document backing the given editor, or {@code null} when it + * cannot be resolved. + * + *

+ * {@link ITextEditor#getDocumentProvider()} is an optional part of the Eclipse + * API and third party editors may return {@code null} from it. Dereferencing it + * unconditionally throws a {@link NullPointerException} out of the + * {@code IPartListener2} callback, which the workbench surfaces as a generic + * error dialog on every editor activation. + *

+ */ + private static IDocument resolveDocument(final ITextEditor editor) { + if (editor == null) { + return null; + } + var documentProvider = editor.getDocumentProvider(); + if (documentProvider == null) { + return null; + } + return documentProvider.getDocument(editor.getEditorInput()); + } + private void detachDocumentListenerFromLastActiveDocument() { if (activeDocument != null) { activeDocument.removeDocumentListener(docListener); @@ -107,7 +133,7 @@ public void onStart() { public void run() { var editor = getActiveTextEditor(); if (editor != null) { - var document = editor.getDocumentProvider().getDocument(editor.getEditorInput()); + var document = resolveDocument(editor); if (document == null) { Display.getDefault().timerExec(1000, this); return; diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/util/QEclipseEditorUtils.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/util/QEclipseEditorUtils.java index c59f3cf27..96c9a8a74 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/util/QEclipseEditorUtils.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/util/QEclipseEditorUtils.java @@ -76,6 +76,27 @@ private static ISelection getSelection(final ITextEditor textEditor) { return textEditor.getSelectionProvider().getSelection(); } + /** + * Resolves the document backing the given editor, or {@code null} when it + * cannot be resolved. + * + *

+ * {@link ITextEditor#getDocumentProvider()} is an optional part of the Eclipse + * API: third party editors may return {@code null} from it. Callers must + * therefore treat both the provider and the document it returns as nullable. + *

+ */ + private static IDocument getDocument(final ITextEditor editor) { + if (editor == null) { + return null; + } + var documentProvider = editor.getDocumentProvider(); + if (documentProvider == null) { + return null; + } + return documentProvider.getDocument(editor.getEditorInput()); + } + private static ITextEditor asTextEditor(final IEditorPart editorPart) { if (editorPart instanceof ITextEditor) { return (ITextEditor) editorPart; @@ -194,7 +215,10 @@ public static Optional getSelectionRange(final ITextEditor editor) { return Optional.empty(); } ISelection selection = getSelection(editor); - var document = editor.getDocumentProvider().getDocument(editor.getEditorInput()); + var document = getDocument(editor); + if (document == null) { + return Optional.empty(); + } if (selection instanceof ITextSelection textSelection) { try { int startOffset = textSelection.getOffset(); @@ -232,7 +256,10 @@ public static Optional insertAtCursor(final String text) { } var selection = editor.getSelectionProvider().getSelection(); - var document = editor.getDocumentProvider().getDocument(editor.getEditorInput()); + var document = getDocument(editor); + if (document == null) { + return Optional.empty(); + } if (selection instanceof ITextSelection textSelection) { try { int startOffset = textSelection.getOffset(); diff --git a/plugin/tst/software/aws/toolkits/eclipse/amazonq/util/AutoTriggerPartListenerTest.java b/plugin/tst/software/aws/toolkits/eclipse/amazonq/util/AutoTriggerPartListenerTest.java index c4f850a9b..8dffb03d9 100644 --- a/plugin/tst/software/aws/toolkits/eclipse/amazonq/util/AutoTriggerPartListenerTest.java +++ b/plugin/tst/software/aws/toolkits/eclipse/amazonq/util/AutoTriggerPartListenerTest.java @@ -3,6 +3,7 @@ package software.aws.toolkits.eclipse.amazonq.util; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.RETURNS_DEEP_STUBS; import static org.mockito.Mockito.atLeast; @@ -27,6 +28,7 @@ import org.eclipse.ui.IEditorInput; import org.eclipse.ui.IWorkbenchPart; import org.eclipse.ui.IWorkbenchPartReference; +import org.eclipse.ui.texteditor.IDocumentProvider; import org.eclipse.ui.texteditor.ITextEditor; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; @@ -151,6 +153,52 @@ void testShutdownRoutine() { verify(docMock, times(removeDocListenerInvocNumber + 1)).removeDocumentListener(docListenerMock); } + @SuppressWarnings({ "rawtypes", "unchecked" }) + @Test + void testPartActivateWithNullDocumentProvider() { + // Test case: an ITextEditor that returns null from getDocumentProvider() must + // be ignored instead of throwing out of the part listener callback + AutoTriggerPartListener partListener = new AutoTriggerPartListener(docListenerMock); + ITextEditor editorWithoutProvider = mock(ITextEditor.class); + when(editorWithoutProvider.getDocumentProvider()).thenReturn(null); + IWorkbenchPartReference partRefMock = mock(IWorkbenchPartReference.class); + when(partRefMock.getPart(any(Boolean.class))).thenReturn(editorWithoutProvider); + + int addDocListenerInvocNumber = countDocInvocations("addDocumentListener"); + int removeDocListenerInvocNumber = countDocInvocations("removeDocumentListener"); + + assertDoesNotThrow(() -> partListener.partActivated(partRefMock)); + verify(docMock, times(addDocListenerInvocNumber)).addDocumentListener(docListenerMock); + + // Test case: no active document was recorded, so deactivation is also a no-op + assertDoesNotThrow(() -> partListener.partDeactivated(partRefMock)); + verify(docMock, times(removeDocListenerInvocNumber)).removeDocumentListener(docListenerMock); + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + @Test + void testPartActivateWithNullDocument() { + // Test case: a provider that returns a null document must be ignored rather + // than dereferenced when attaching the document listener + AutoTriggerPartListener partListener = new AutoTriggerPartListener(docListenerMock); + IDocumentProvider providerWithoutDocument = mock(IDocumentProvider.class); + when(providerWithoutDocument.getDocument(any())).thenReturn(null); + ITextEditor editorWithoutDocument = mock(ITextEditor.class); + when(editorWithoutDocument.getDocumentProvider()).thenReturn(providerWithoutDocument); + IWorkbenchPartReference partRefMock = mock(IWorkbenchPartReference.class); + when(partRefMock.getPart(any(Boolean.class))).thenReturn(editorWithoutDocument); + + int addDocListenerInvocNumber = countDocInvocations("addDocumentListener"); + + assertDoesNotThrow(() -> partListener.partActivated(partRefMock)); + verify(docMock, times(addDocListenerInvocNumber)).addDocumentListener(docListenerMock); + } + + private static int countDocInvocations(final String methodName) { + return (int) mockingDetails(docMock).getInvocations().stream() + .filter(inv -> inv.getMethod().getName().equals(methodName)).count(); + } + static void mockEditor() { when(editorMock.getDocumentProvider().getDocument(any(IEditorInput.class))).thenReturn(docMock); }