From c7e576eadf53bec7a99ed8a43767f4fd730d1db3 Mon Sep 17 00:00:00 2001 From: laileni Date: Tue, 24 Mar 2026 15:19:49 -0700 Subject: [PATCH 1/3] fix(amazonq): adding openTabFilepaths to inline completion requests --- .../lsp/model/InlineCompletionParams.java | 11 ++ .../amazonq/util/InlineCompletionUtils.java | 8 + .../amazonq/util/QEclipseEditorUtils.java | 33 ++++ .../util/InlineCompletionUtilsTest.java | 124 ++++++++++++ ...EditorUtilsGetOpenEditorFilePathsTest.java | 176 ++++++++++++++++++ 5 files changed, 352 insertions(+) create mode 100644 plugin/tst/software/aws/toolkits/eclipse/amazonq/util/InlineCompletionUtilsTest.java create mode 100644 plugin/tst/software/aws/toolkits/eclipse/amazonq/util/QEclipseEditorUtilsGetOpenEditorFilePathsTest.java diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/model/InlineCompletionParams.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/model/InlineCompletionParams.java index bfc6c7520..c99d83b6f 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/model/InlineCompletionParams.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/model/InlineCompletionParams.java @@ -3,11 +3,14 @@ package software.aws.toolkits.eclipse.amazonq.lsp.model; +import java.util.List; + import org.eclipse.lsp4j.TextDocumentPositionAndWorkDoneProgressParams; public class InlineCompletionParams extends TextDocumentPositionAndWorkDoneProgressParams { private InlineCompletionContext context; + private List openTabFilepaths; public final InlineCompletionContext getContext() { return context; @@ -17,4 +20,12 @@ public final void setContext(final InlineCompletionContext context) { this.context = context; } + public final List getOpenTabFilepaths() { + return openTabFilepaths; + } + + public final void setOpenTabFilepaths(final List openTabFilepaths) { + this.openTabFilepaths = openTabFilepaths; + } + } diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/util/InlineCompletionUtils.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/util/InlineCompletionUtils.java index 4fab8e1c3..8c207dd71 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/util/InlineCompletionUtils.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/util/InlineCompletionUtils.java @@ -3,6 +3,8 @@ package software.aws.toolkits.eclipse.amazonq.util; +import java.util.List; + import org.eclipse.jface.text.BadLocationException; import org.eclipse.jface.text.ITextViewer; import org.eclipse.lsp4j.Position; @@ -41,6 +43,12 @@ public static InlineCompletionParams cwParamsFromContext(final ITextEditor edito invocationPosition.setLine(startLine); invocationPosition.setCharacter(lineOffset); params.setPosition(invocationPosition); + + List openTabFilepaths = QEclipseEditorUtils.getOpenEditorFilePaths(); + if (!openTabFilepaths.isEmpty()) { + params.setOpenTabFilepaths(openTabFilepaths); + } + return params; } } 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 d4c3e00ff..c59f3cf27 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/util/QEclipseEditorUtils.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/util/QEclipseEditorUtils.java @@ -6,6 +6,7 @@ import static software.aws.toolkits.eclipse.amazonq.util.QConstants.Q_INLINE_HINT_TEXT_STYLE; import java.nio.file.Paths; +import java.util.ArrayList; import java.util.List; import java.util.Optional; import java.util.function.Consumer; @@ -37,6 +38,7 @@ import org.eclipse.swt.widgets.Shell; import org.eclipse.ui.IEditorInput; import org.eclipse.ui.IEditorPart; +import org.eclipse.ui.IEditorReference; import org.eclipse.ui.IFileEditorInput; import org.eclipse.ui.IWorkbenchPage; import org.eclipse.ui.IWorkbenchWindow; @@ -156,6 +158,37 @@ private static String getOpenFilePath(final IEditorInput editorInput) { } } + /** + * Returns file paths for all currently open editor tabs, excluding in-memory + * editors. Used to provide supplemental context for inline completions. + */ + public static List getOpenEditorFilePaths() { + List filePaths = new ArrayList<>(); + try { + IWorkbenchPage page = getActivePage(); + if (page == null) { + return filePaths; + } + for (IEditorReference editorRef : page.getEditorReferences()) { + try { + IEditorInput input = editorRef.getEditorInput(); + if (input instanceof InMemoryInput) { + continue; + } + String path = getOpenFilePath(input); + if (path != null && !path.isEmpty()) { + filePaths.add(path); + } + } catch (Exception e) { + Activator.getLogger().warn("Skipping editor tab: unable to resolve file path", e); + } + } + } catch (Exception e) { + Activator.getLogger().error("Error collecting open editor file paths", e); + } + return filePaths; + } + public static Optional getSelectionRange(final ITextEditor editor) { if (editor == null) { return Optional.empty(); diff --git a/plugin/tst/software/aws/toolkits/eclipse/amazonq/util/InlineCompletionUtilsTest.java b/plugin/tst/software/aws/toolkits/eclipse/amazonq/util/InlineCompletionUtilsTest.java new file mode 100644 index 000000000..28554671d --- /dev/null +++ b/plugin/tst/software/aws/toolkits/eclipse/amazonq/util/InlineCompletionUtilsTest.java @@ -0,0 +1,124 @@ +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package software.aws.toolkits.eclipse.amazonq.util; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.when; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +import org.eclipse.jface.text.IDocument; +import org.eclipse.jface.text.ITextViewer; +import org.eclipse.ui.IEditorInput; +import org.eclipse.ui.texteditor.ITextEditor; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; + +import software.aws.toolkits.eclipse.amazonq.lsp.model.InlineCompletionParams; +import software.aws.toolkits.eclipse.amazonq.lsp.model.InlineCompletionTriggerKind; + +public class InlineCompletionUtilsTest { + + private static MockedStatic editorUtilsMock; + + @BeforeAll + public static void setUp() { + editorUtilsMock = mockStatic(QEclipseEditorUtils.class); + } + + @AfterAll + public static void tearDown() { + if (editorUtilsMock != null) { + editorUtilsMock.close(); + } + } + + @Test + void testCwParamsIncludesOpenTabFilepaths() throws Exception { + ITextEditor editor = mock(ITextEditor.class); + IEditorInput editorInput = mock(IEditorInput.class); + when(editor.getEditorInput()).thenReturn(editorInput); + + ITextViewer viewer = mock(ITextViewer.class); + IDocument document = mock(IDocument.class); + when(viewer.getDocument()).thenReturn(document); + when(document.getLineOfOffset(0)).thenReturn(0); + when(document.getLineOffset(0)).thenReturn(0); + + List mockPaths = Arrays.asList("/project/src/Foo.java", "/project/src/Bar.java"); + editorUtilsMock.when(() -> QEclipseEditorUtils.getOpenFileUri(any(IEditorInput.class))) + .thenReturn(Optional.of("file:///project/src/Active.java")); + editorUtilsMock.when(QEclipseEditorUtils::getOpenEditorFilePaths) + .thenReturn(mockPaths); + + InlineCompletionParams params = InlineCompletionUtils.cwParamsFromContext( + editor, viewer, 0, InlineCompletionTriggerKind.Invoke); + + assertNotNull(params.getOpenTabFilepaths()); + assertEquals(2, params.getOpenTabFilepaths().size()); + assertTrue(params.getOpenTabFilepaths().contains("/project/src/Foo.java")); + assertTrue(params.getOpenTabFilepaths().contains("/project/src/Bar.java")); + } + + @Test + void testCwParamsOmitsOpenTabFilepathsWhenEmpty() throws Exception { + ITextEditor editor = mock(ITextEditor.class); + IEditorInput editorInput = mock(IEditorInput.class); + when(editor.getEditorInput()).thenReturn(editorInput); + + ITextViewer viewer = mock(ITextViewer.class); + IDocument document = mock(IDocument.class); + when(viewer.getDocument()).thenReturn(document); + when(document.getLineOfOffset(0)).thenReturn(0); + when(document.getLineOffset(0)).thenReturn(0); + + editorUtilsMock.when(() -> QEclipseEditorUtils.getOpenFileUri(any(IEditorInput.class))) + .thenReturn(Optional.of("file:///project/src/Active.java")); + editorUtilsMock.when(QEclipseEditorUtils::getOpenEditorFilePaths) + .thenReturn(Collections.emptyList()); + + InlineCompletionParams params = InlineCompletionUtils.cwParamsFromContext( + editor, viewer, 0, InlineCompletionTriggerKind.Invoke); + + assertNull(params.getOpenTabFilepaths()); + } + + @Test + void testCwParamsSetsCorrectPositionAndContext() throws Exception { + ITextEditor editor = mock(ITextEditor.class); + IEditorInput editorInput = mock(IEditorInput.class); + when(editor.getEditorInput()).thenReturn(editorInput); + + ITextViewer viewer = mock(ITextViewer.class); + IDocument document = mock(IDocument.class); + when(viewer.getDocument()).thenReturn(document); + when(document.getLineOfOffset(25)).thenReturn(2); + when(document.getLineOffset(2)).thenReturn(20); + + editorUtilsMock.when(() -> QEclipseEditorUtils.getOpenFileUri(any(IEditorInput.class))) + .thenReturn(Optional.of("file:///project/src/Test.java")); + editorUtilsMock.when(QEclipseEditorUtils::getOpenEditorFilePaths) + .thenReturn(Arrays.asList("/project/src/Foo.java")); + + InlineCompletionParams params = InlineCompletionUtils.cwParamsFromContext( + editor, viewer, 25, InlineCompletionTriggerKind.Automatic); + + assertEquals(2, params.getPosition().getLine()); + assertEquals(5, params.getPosition().getCharacter()); + assertEquals(InlineCompletionTriggerKind.Automatic, params.getContext().getTriggerKind()); + assertNotNull(params.getTextDocument()); + assertEquals("file:///project/src/Test.java", params.getTextDocument().getUri()); + } +} diff --git a/plugin/tst/software/aws/toolkits/eclipse/amazonq/util/QEclipseEditorUtilsGetOpenEditorFilePathsTest.java b/plugin/tst/software/aws/toolkits/eclipse/amazonq/util/QEclipseEditorUtilsGetOpenEditorFilePathsTest.java new file mode 100644 index 000000000..4d8f8d7d7 --- /dev/null +++ b/plugin/tst/software/aws/toolkits/eclipse/amazonq/util/QEclipseEditorUtilsGetOpenEditorFilePathsTest.java @@ -0,0 +1,176 @@ +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package software.aws.toolkits.eclipse.amazonq.util; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.RETURNS_DEEP_STUBS; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.when; + +import java.net.URI; +import java.util.List; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.runtime.IPath; +import org.eclipse.ui.IEditorInput; +import org.eclipse.ui.IEditorReference; +import org.eclipse.ui.IFileEditorInput; +import org.eclipse.ui.IWorkbench; +import org.eclipse.ui.IWorkbenchPage; +import org.eclipse.ui.IWorkbenchWindow; +import org.eclipse.ui.PlatformUI; +import org.eclipse.ui.ide.FileStoreEditorInput; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; + +import software.aws.toolkits.eclipse.amazonq.editor.InMemoryInput; +import software.aws.toolkits.eclipse.amazonq.plugin.Activator; + +public class QEclipseEditorUtilsGetOpenEditorFilePathsTest { + + private static MockedStatic platformUIMock; + private static MockedStatic activatorMock; + private static IWorkbenchPage mockPage; + + @BeforeAll + public static void setUp() { + platformUIMock = mockStatic(PlatformUI.class); + IWorkbench workbench = mock(IWorkbench.class); + IWorkbenchWindow window = mock(IWorkbenchWindow.class); + mockPage = mock(IWorkbenchPage.class); + + platformUIMock.when(PlatformUI::getWorkbench).thenReturn(workbench); + when(workbench.getActiveWorkbenchWindow()).thenReturn(window); + when(window.getActivePage()).thenReturn(mockPage); + + activatorMock = mockStatic(Activator.class); + LoggingService loggingService = mock(LoggingService.class); + activatorMock.when(Activator::getLogger).thenReturn(loggingService); + } + + @AfterAll + public static void tearDown() { + if (platformUIMock != null) { + platformUIMock.close(); + } + if (activatorMock != null) { + activatorMock.close(); + } + } + + @Test + void testReturnsFilePathsFromFileStoreEditors() throws Exception { + FileStoreEditorInput input1 = mock(FileStoreEditorInput.class); + when(input1.getURI()).thenReturn(new URI("file:///project/src/Foo.java")); + + FileStoreEditorInput input2 = mock(FileStoreEditorInput.class); + when(input2.getURI()).thenReturn(new URI("file:///project/src/Bar.java")); + + IEditorReference ref1 = mock(IEditorReference.class); + when(ref1.getEditorInput()).thenReturn(input1); + + IEditorReference ref2 = mock(IEditorReference.class); + when(ref2.getEditorInput()).thenReturn(input2); + + when(mockPage.getEditorReferences()).thenReturn(new IEditorReference[]{ref1, ref2}); + + List paths = QEclipseEditorUtils.getOpenEditorFilePaths(); + + assertNotNull(paths); + assertEquals(2, paths.size()); + assertTrue(paths.contains("/project/src/Foo.java")); + assertTrue(paths.contains("/project/src/Bar.java")); + } + + @Test + void testReturnsFilePathsFromIFileEditorInputs() throws Exception { + IFileEditorInput input = mock(IFileEditorInput.class); + IFile file = mock(IFile.class); + IPath rawLocation = mock(IPath.class); + when(input.getFile()).thenReturn(file); + when(file.getRawLocation()).thenReturn(rawLocation); + when(rawLocation.toOSString()).thenReturn("/project/src/Model.java"); + when(mockPage.findEditor(input)).thenReturn(null); + + IEditorReference ref = mock(IEditorReference.class); + when(ref.getEditorInput()).thenReturn(input); + + when(mockPage.getEditorReferences()).thenReturn(new IEditorReference[]{ref}); + + List paths = QEclipseEditorUtils.getOpenEditorFilePaths(); + + assertNotNull(paths); + assertEquals(1, paths.size()); + assertTrue(paths.contains("/project/src/Model.java")); + } + + @Test + void testSkipsInMemoryEditors() throws Exception { + InMemoryInput inMemoryInput = mock(InMemoryInput.class); + IEditorReference inMemoryRef = mock(IEditorReference.class); + when(inMemoryRef.getEditorInput()).thenReturn(inMemoryInput); + + FileStoreEditorInput fileInput = mock(FileStoreEditorInput.class); + when(fileInput.getURI()).thenReturn(new URI("file:///project/src/Real.java")); + IEditorReference fileRef = mock(IEditorReference.class); + when(fileRef.getEditorInput()).thenReturn(fileInput); + + when(mockPage.getEditorReferences()).thenReturn(new IEditorReference[]{inMemoryRef, fileRef}); + + List paths = QEclipseEditorUtils.getOpenEditorFilePaths(); + + assertEquals(1, paths.size()); + assertTrue(paths.contains("/project/src/Real.java")); + } + + @Test + void testReturnsEmptyListWhenNoEditorsOpen() { + when(mockPage.getEditorReferences()).thenReturn(new IEditorReference[]{}); + + List paths = QEclipseEditorUtils.getOpenEditorFilePaths(); + + assertNotNull(paths); + assertTrue(paths.isEmpty()); + } + + @Test + void testSkipsEditorsWithUnresolvableInput() throws Exception { + IEditorReference badRef = mock(IEditorReference.class); + when(badRef.getEditorInput()).thenThrow(new RuntimeException("Cannot resolve")); + + FileStoreEditorInput goodInput = mock(FileStoreEditorInput.class); + when(goodInput.getURI()).thenReturn(new URI("file:///project/src/Good.java")); + IEditorReference goodRef = mock(IEditorReference.class); + when(goodRef.getEditorInput()).thenReturn(goodInput); + + when(mockPage.getEditorReferences()).thenReturn(new IEditorReference[]{badRef, goodRef}); + + List paths = QEclipseEditorUtils.getOpenEditorFilePaths(); + + assertEquals(1, paths.size()); + assertTrue(paths.contains("/project/src/Good.java")); + } + + @Test + void testReturnsEmptyListWhenActivePageIsNull() { + IWorkbench workbench = mock(IWorkbench.class); + IWorkbenchWindow window = mock(IWorkbenchWindow.class); + platformUIMock.when(PlatformUI::getWorkbench).thenReturn(workbench); + when(workbench.getActiveWorkbenchWindow()).thenReturn(window); + when(window.getActivePage()).thenReturn(null); + + List paths = QEclipseEditorUtils.getOpenEditorFilePaths(); + + assertNotNull(paths); + assertTrue(paths.isEmpty()); + + // Restore for other tests + when(window.getActivePage()).thenReturn(mockPage); + } +} From 54d32d00f3ca26b0c27d2c0066ec22ee4583c4a4 Mon Sep 17 00:00:00 2001 From: laileni Date: Tue, 24 Mar 2026 17:20:34 -0700 Subject: [PATCH 2/3] fix(amazonq): removing unused imports --- .../util/QEclipseEditorUtilsGetOpenEditorFilePathsTest.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/plugin/tst/software/aws/toolkits/eclipse/amazonq/util/QEclipseEditorUtilsGetOpenEditorFilePathsTest.java b/plugin/tst/software/aws/toolkits/eclipse/amazonq/util/QEclipseEditorUtilsGetOpenEditorFilePathsTest.java index 4d8f8d7d7..f114e732b 100644 --- a/plugin/tst/software/aws/toolkits/eclipse/amazonq/util/QEclipseEditorUtilsGetOpenEditorFilePathsTest.java +++ b/plugin/tst/software/aws/toolkits/eclipse/amazonq/util/QEclipseEditorUtilsGetOpenEditorFilePathsTest.java @@ -6,7 +6,6 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.Mockito.RETURNS_DEEP_STUBS; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.when; @@ -16,7 +15,6 @@ import org.eclipse.core.resources.IFile; import org.eclipse.core.runtime.IPath; -import org.eclipse.ui.IEditorInput; import org.eclipse.ui.IEditorReference; import org.eclipse.ui.IFileEditorInput; import org.eclipse.ui.IWorkbench; From 122444c06cdf0097354807c7f775f681f8d7eb76 Mon Sep 17 00:00:00 2001 From: laileni Date: Thu, 28 May 2026 18:33:03 -0700 Subject: [PATCH 3/3] fix: block F5/Refresh in chat webview to prevent blank screen --- .../amazonq/views/AmazonQChatWebview.java | 5 ++ .../views/RefreshBlockingKeyListener.java | 31 ++++++++++++ .../RefreshBlockingLocationListener.java | 38 ++++++++++++++ .../views/RefreshBlockingKeyListenerTest.java | 49 +++++++++++++++++++ .../RefreshBlockingLocationListenerTest.java | 48 ++++++++++++++++++ 5 files changed, 171 insertions(+) create mode 100644 plugin/src/software/aws/toolkits/eclipse/amazonq/views/RefreshBlockingKeyListener.java create mode 100644 plugin/src/software/aws/toolkits/eclipse/amazonq/views/RefreshBlockingLocationListener.java create mode 100644 plugin/tst/software/aws/toolkits/eclipse/amazonq/views/RefreshBlockingKeyListenerTest.java create mode 100644 plugin/tst/software/aws/toolkits/eclipse/amazonq/views/RefreshBlockingLocationListenerTest.java diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/views/AmazonQChatWebview.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/views/AmazonQChatWebview.java index 7539dfde2..f33be9df7 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/views/AmazonQChatWebview.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/views/AmazonQChatWebview.java @@ -62,6 +62,11 @@ public void completed(final ProgressEvent event) { }); webViewAssetProvider.setContent(browser); + + // Block F5/Refresh - the webview uses setText() with no backing URL, + // so browser refresh navigates to about:blank or file:/// causing a blank screen. + browser.addLocationListener(new RefreshBlockingLocationListener()); + browser.addKeyListener(new RefreshBlockingKeyListener()); } super.setupView(parent); diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/views/RefreshBlockingKeyListener.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/views/RefreshBlockingKeyListener.java new file mode 100644 index 000000000..ef0c45add --- /dev/null +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/views/RefreshBlockingKeyListener.java @@ -0,0 +1,31 @@ +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package software.aws.toolkits.eclipse.amazonq.views; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.KeyAdapter; +import org.eclipse.swt.events.KeyEvent; + +/** + * Blocks F5 keypress to prevent browser refresh in the chat webview. + * The webview uses setText() with no backing URL, so F5 would navigate + * to about:blank, wiping the UI. + */ +final class RefreshBlockingKeyListener extends KeyAdapter { + + @Override + public void keyPressed(final KeyEvent e) { + if (shouldBlockKey(e.keyCode)) { + e.doit = false; + } + } + + /** + * Determines whether the given key code should be blocked. + * Package-private for testability. + */ + static boolean shouldBlockKey(final int keyCode) { + return keyCode == SWT.F5; + } +} diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/views/RefreshBlockingLocationListener.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/views/RefreshBlockingLocationListener.java new file mode 100644 index 000000000..e52185070 --- /dev/null +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/views/RefreshBlockingLocationListener.java @@ -0,0 +1,38 @@ +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package software.aws.toolkits.eclipse.amazonq.views; + +import org.eclipse.swt.browser.LocationAdapter; +import org.eclipse.swt.browser.LocationEvent; + +/** + * Blocks browser navigation to prevent the chat webview from going blank on + * refresh. The webview uses setText() with no backing URL, so any refresh or + * navigation attempt will leave the rendered content. On different platforms, + * refresh may navigate to about:blank (Windows/Linux) or file:/// (macOS WebKit). + * This listener blocks all navigation since the webview should never navigate away. + */ +final class RefreshBlockingLocationListener extends LocationAdapter { + + @Override + public void changing(final LocationEvent event) { + if (shouldBlockNavigation(event.location)) { + event.doit = false; + } + } + + /** + * Determines whether navigation to the given location should be blocked. + * Blocks about:blank and file:// URLs which are triggered by refresh on + * different platforms. The webview content is set via setText() and should + * never navigate to any URL. + * Package-private for testability. + */ + static boolean shouldBlockNavigation(final String location) { + if (location == null || location.isEmpty()) { + return false; + } + return "about:blank".equals(location) || location.startsWith("file:"); + } +} diff --git a/plugin/tst/software/aws/toolkits/eclipse/amazonq/views/RefreshBlockingKeyListenerTest.java b/plugin/tst/software/aws/toolkits/eclipse/amazonq/views/RefreshBlockingKeyListenerTest.java new file mode 100644 index 000000000..69ca6fd8c --- /dev/null +++ b/plugin/tst/software/aws/toolkits/eclipse/amazonq/views/RefreshBlockingKeyListenerTest.java @@ -0,0 +1,49 @@ +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package software.aws.toolkits.eclipse.amazonq.views; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.eclipse.swt.SWT; +import org.junit.jupiter.api.Test; + +public final class RefreshBlockingKeyListenerTest { + + @Test + void testBlocksF5KeyPress() { + assertTrue(RefreshBlockingKeyListener.shouldBlockKey(SWT.F5), + "F5 keypress should be blocked"); + } + + @Test + void testAllowsF4KeyPress() { + assertFalse(RefreshBlockingKeyListener.shouldBlockKey(SWT.F4), + "F4 keypress should not be blocked"); + } + + @Test + void testAllowsF6KeyPress() { + assertFalse(RefreshBlockingKeyListener.shouldBlockKey(SWT.F6), + "F6 keypress should not be blocked"); + } + + @Test + void testAllowsEnterKey() { + assertFalse(RefreshBlockingKeyListener.shouldBlockKey(SWT.CR), + "Enter key should not be blocked"); + } + + @Test + void testAllowsRegularCharacterKey() { + assertFalse(RefreshBlockingKeyListener.shouldBlockKey('a'), + "Regular character keys should not be blocked"); + } + + @Test + void testAllowsEscapeKey() { + assertFalse(RefreshBlockingKeyListener.shouldBlockKey(SWT.ESC), + "Escape key should not be blocked"); + } +} diff --git a/plugin/tst/software/aws/toolkits/eclipse/amazonq/views/RefreshBlockingLocationListenerTest.java b/plugin/tst/software/aws/toolkits/eclipse/amazonq/views/RefreshBlockingLocationListenerTest.java new file mode 100644 index 000000000..33be28eaa --- /dev/null +++ b/plugin/tst/software/aws/toolkits/eclipse/amazonq/views/RefreshBlockingLocationListenerTest.java @@ -0,0 +1,48 @@ +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package software.aws.toolkits.eclipse.amazonq.views; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +public final class RefreshBlockingLocationListenerTest { + + @Test + void testBlocksNavigationToAboutBlank() { + assertTrue(RefreshBlockingLocationListener.shouldBlockNavigation("about:blank"), + "Navigation to about:blank should be blocked"); + } + + @Test + void testBlocksNavigationToFileProtocol() { + assertTrue(RefreshBlockingLocationListener.shouldBlockNavigation("file:///"), + "Navigation to file:/// should be blocked (macOS WebKit reload)"); + } + + @Test + void testBlocksNavigationToFileProtocolWithPath() { + assertTrue(RefreshBlockingLocationListener.shouldBlockNavigation("file:///tmp/test.html"), + "Navigation to file:// URLs should be blocked"); + } + + @Test + void testAllowsNavigationToHttpsUrl() { + assertFalse(RefreshBlockingLocationListener.shouldBlockNavigation("https://example.com"), + "Navigation to HTTPS URLs should be allowed"); + } + + @Test + void testAllowsNavigationWhenLocationIsNull() { + assertFalse(RefreshBlockingLocationListener.shouldBlockNavigation(null), + "Navigation should be allowed when location is null"); + } + + @Test + void testAllowsNavigationToEmptyString() { + assertFalse(RefreshBlockingLocationListener.shouldBlockNavigation(""), + "Navigation should be allowed when location is empty"); + } +}