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..f114e732b --- /dev/null +++ b/plugin/tst/software/aws/toolkits/eclipse/amazonq/util/QEclipseEditorUtilsGetOpenEditorFilePathsTest.java @@ -0,0 +1,174 @@ +// 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.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.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); + } +}