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 dcbf7358..cad325c4 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 c59f3cf2..96c9a8a7 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 c4f850a9..8dffb03d 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);
}