From 7ba79c8097cbb886cbc624b2a617cadb1658da37 Mon Sep 17 00:00:00 2001 From: lytreallynb Date: Wed, 12 Aug 2026 15:42:42 -0400 Subject: [PATCH 1/2] feat: add Copilot Java quick assist --- .../META-INF/MANIFEST.MF | 3 +- .../quickfix/QuickAssistExtensionTests.java | 44 ++++++ .../QuickFixProcessorSupportTests.java | 148 ++++++++++++++++++ .../META-INF/MANIFEST.MF | 1 + .../plugin.properties | 3 +- com.microsoft.copilot.eclipse.ui/plugin.xml | 8 + .../copilot/eclipse/ui/i18n/Messages.java | 4 +- .../eclipse/ui/i18n/messages.properties | 2 + .../ui/quickfix/CopilotQuickFixProposal.java | 59 +++++++ .../JavaCopilotQuickAssistProcessor.java | 81 ++++++++++ .../ui/quickfix/QuickFixProcessorSupport.java | 138 ++++++++++++++++ 11 files changed, 488 insertions(+), 3 deletions(-) create mode 100644 com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/quickfix/QuickAssistExtensionTests.java create mode 100644 com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/quickfix/QuickFixProcessorSupportTests.java create mode 100644 com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/quickfix/CopilotQuickFixProposal.java create mode 100644 com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/quickfix/JavaCopilotQuickAssistProcessor.java create mode 100644 com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/quickfix/QuickFixProcessorSupport.java diff --git a/com.microsoft.copilot.eclipse.ui.test/META-INF/MANIFEST.MF b/com.microsoft.copilot.eclipse.ui.test/META-INF/MANIFEST.MF index e8770d2a3..ab85b21d7 100644 --- a/com.microsoft.copilot.eclipse.ui.test/META-INF/MANIFEST.MF +++ b/com.microsoft.copilot.eclipse.ui.test/META-INF/MANIFEST.MF @@ -11,4 +11,5 @@ Import-Package: org.objenesis;version="[3.4.0,4.0.0)" Require-Bundle: junit-jupiter-api;bundle-version="5.10.1", junit-jupiter-params;bundle-version="5.10.1", org.mockito.mockito-core;bundle-version="5.14.2", - org.mockito.junit-jupiter;bundle-version="5.10.2" + org.mockito.junit-jupiter;bundle-version="5.10.2", + org.eclipse.jdt.ui diff --git a/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/quickfix/QuickAssistExtensionTests.java b/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/quickfix/QuickAssistExtensionTests.java new file mode 100644 index 000000000..0aa1c8b99 --- /dev/null +++ b/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/quickfix/QuickAssistExtensionTests.java @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +package com.microsoft.copilot.eclipse.ui.quickfix; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + +import java.util.Arrays; + +import org.eclipse.core.runtime.IConfigurationElement; +import org.eclipse.core.runtime.Platform; +import org.junit.jupiter.api.Test; + +class QuickAssistExtensionTests { + + @Test + void registersJavaQuickAssistProcessor() throws Exception { + IConfigurationElement element = findProcessor("org.eclipse.jdt.ui.quickAssistProcessors", + JavaCopilotQuickAssistProcessor.class.getName()); + + assertInstanceOf(JavaCopilotQuickAssistProcessor.class, element.createExecutableExtension("class")); + } + + @Test + void hidesProposalWhenCopilotIsUnavailable() throws Exception { + JavaCopilotQuickAssistProcessor processor = new JavaCopilotQuickAssistProcessor(() -> false); + + assertFalse(processor.hasAssists(null)); + assertNull(processor.getAssists(null, null)); + } + + private IConfigurationElement findProcessor(String extensionPointId, String className) { + IConfigurationElement element = Arrays.stream( + Platform.getExtensionRegistry().getConfigurationElementsFor(extensionPointId)) + .filter(candidate -> className.equals(candidate.getAttribute("class"))) + .findFirst() + .orElse(null); + assertNotNull(element, () -> "Missing quick assist processor registration for " + className); + return element; + } +} diff --git a/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/quickfix/QuickFixProcessorSupportTests.java b/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/quickfix/QuickFixProcessorSupportTests.java new file mode 100644 index 000000000..eaa1ace9e --- /dev/null +++ b/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/quickfix/QuickFixProcessorSupportTests.java @@ -0,0 +1,148 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +package com.microsoft.copilot.eclipse.ui.quickfix; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.ByteArrayInputStream; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IMarker; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.jface.text.Document; +import org.eclipse.jface.text.IDocument; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import com.microsoft.copilot.eclipse.ui.UiConstants; +import com.microsoft.copilot.eclipse.ui.i18n.Messages; + +class QuickFixProcessorSupportTests { + private static final String CONTENT = "first line\nsecond problem\nthird problem\n"; + + private IProject project; + private IFile file; + private IDocument document; + + @BeforeEach + void setUp() throws Exception { + project = ResourcesPlugin.getWorkspace().getRoot().getProject("copilot-quick-fix-tests"); + if (project.exists()) { + project.delete(true, true, null); + } + project.create(null); + project.open(null); + + file = project.getFile("problems.txt"); + file.create(new ByteArrayInputStream(CONTENT.getBytes(StandardCharsets.UTF_8)), true, null); + document = new Document(CONTENT); + } + + @AfterEach + void tearDown() throws Exception { + if (project != null && project.exists()) { + project.delete(true, true, null); + } + } + + @Test + void findsProblemAtCaret() throws Exception { + createMarker("Fix the second line", 11, 25, 2); + + assertEquals(List.of("Fix the second line"), + QuickFixProcessorSupport.findProblemMessages(file, document, 18, 0)); + assertTrue(QuickFixProcessorSupport.findProblemMessages(file, document, 10, 0).isEmpty()); + assertTrue(QuickFixProcessorSupport.findProblemMessages(file, document, 25, 0).isEmpty()); + } + + @Test + void findsAllProblemsOverlappingSelectionInSourceOrder() throws Exception { + createMarker("Third problem", 26, 39, 3); + createMarker("Second problem", 11, 25, 2); + createMarker("Second problem", 15, 20, 2); + + assertEquals(List.of("Second problem", "Third problem"), + QuickFixProcessorSupport.findProblemMessages(file, document, 11, 28)); + } + + @Test + void supportsZeroLengthAndLineOnlyMarkers() throws Exception { + createMarker("Insertion problem", 11, 11, 2); + IMarker lineMarker = file.createMarker(IMarker.PROBLEM); + lineMarker.setAttribute(IMarker.MESSAGE, "Line problem"); + lineMarker.setAttribute(IMarker.LINE_NUMBER, 3); + + assertEquals(List.of("Insertion problem"), + QuickFixProcessorSupport.findProblemMessages(file, document, 11, 0)); + assertEquals(List.of("Line problem"), + QuickFixProcessorSupport.findProblemMessages(file, document, 30, 0)); + } + + @Test + void ordersLineOnlyAndCharacterRangeMarkersBySourcePosition() throws Exception { + createMarker("Third problem", 26, 39, 3); + IMarker lineMarker = file.createMarker(IMarker.PROBLEM); + lineMarker.setAttribute(IMarker.MESSAGE, "Second problem"); + lineMarker.setAttribute(IMarker.LINE_NUMBER, 2); + + assertEquals(List.of("Second problem", "Third problem"), + QuickFixProcessorSupport.findProblemMessages(file, document, 11, 28)); + } + + @Test + void ignoresMarkersWithoutMessageAndInvalidInvocationOffsets() throws Exception { + IMarker marker = file.createMarker(IMarker.PROBLEM); + marker.setAttribute(IMarker.CHAR_START, 0); + marker.setAttribute(IMarker.CHAR_END, 5); + + assertTrue(QuickFixProcessorSupport.findProblemMessages(file, document, 2, 0).isEmpty()); + assertTrue(QuickFixProcessorSupport.findProblemMessages(file, document, -1, 0).isEmpty()); + assertTrue(QuickFixProcessorSupport.findProblemMessages(file, document, document.getLength() + 1, 0) + .isEmpty()); + } + + @Test + void handlesInvalidAndVeryLargeSelectionLengths() throws Exception { + createMarker("Second problem", 11, 25, 2); + + assertTrue(QuickFixProcessorSupport.findProblemMessages(file, document, 10, -1).isEmpty()); + assertEquals(List.of("Second problem"), + QuickFixProcessorSupport.findProblemMessages(file, document, 10, Integer.MAX_VALUE)); + } + + @Test + void buildsPromptAndProposalPrefillsIt() { + String expectedPrompt = Messages.quickFix_prompt + System.lineSeparator() + System.lineSeparator() + + "- First problem" + + System.lineSeparator() + "- Second problem"; + String prompt = QuickFixProcessorSupport.buildPrompt(List.of("First problem", "Second problem")); + assertEquals(expectedPrompt, prompt); + + AtomicReference openedPrompt = new AtomicReference<>(); + CopilotQuickFixProposal proposal = new CopilotQuickFixProposal(prompt, openedPrompt::set); + proposal.apply(document); + + assertEquals(expectedPrompt, openedPrompt.get()); + assertEquals(Messages.quickFix_fixWithCopilot, proposal.getDisplayString()); + + Map parameters = QuickFixProcessorSupport.createOpenChatParameters(prompt); + assertEquals(expectedPrompt, parameters.get(UiConstants.OPEN_CHAT_VIEW_INPUT_VALUE)); + assertEquals(Boolean.FALSE.toString(), parameters.get(UiConstants.OPEN_CHAT_VIEW_AUTO_SEND)); + } + + private void createMarker(String message, int start, int end, int line) throws Exception { + IMarker marker = file.createMarker(IMarker.PROBLEM); + marker.setAttribute(IMarker.MESSAGE, message); + marker.setAttribute(IMarker.CHAR_START, start); + marker.setAttribute(IMarker.CHAR_END, end); + marker.setAttribute(IMarker.LINE_NUMBER, line); + } +} diff --git a/com.microsoft.copilot.eclipse.ui/META-INF/MANIFEST.MF b/com.microsoft.copilot.eclipse.ui/META-INF/MANIFEST.MF index 55a18733d..a7cc5c25d 100644 --- a/com.microsoft.copilot.eclipse.ui/META-INF/MANIFEST.MF +++ b/com.microsoft.copilot.eclipse.ui/META-INF/MANIFEST.MF @@ -68,4 +68,5 @@ Require-Bundle: com.microsoft.copilot.eclipse.core;bundle-version="0.21.0", org.eclipse.ui.editors, org.eclipse.debug.core;resolution:=optional, org.eclipse.jdt.core;resolution:=optional, + org.eclipse.jdt.ui;resolution:=optional, org.eclipse.jdt.debug;resolution:=optional diff --git a/com.microsoft.copilot.eclipse.ui/plugin.properties b/com.microsoft.copilot.eclipse.ui/plugin.properties index 636fc1fb2..ae9fc0c91 100644 --- a/com.microsoft.copilot.eclipse.ui/plugin.properties +++ b/com.microsoft.copilot.eclipse.ui/plugin.properties @@ -9,6 +9,7 @@ command.showChatView.name=Open Copilot Chat command.openChatView.inputValue=Default input value for the opened chat view command.openChatView.autoSend=Open Chat View with Auto Send command.openChatView.mode=Chat Mode for Opened Chat View +quickAssist.processor.name=GitHub Copilot Quick Assist Processor command.signOutFromGitHub.name=Sign Out from GitHub command.configureCopilotSettings.name=Activate GitHub Copilot Account... command.openPreferencesPage.name=Edit Preferences... @@ -48,4 +49,4 @@ theme.category.label=GitHub Copilot theme.category.description=Font and color settings for GitHub Copilot theme.chatFont.label=Chat Font theme.chatFont.description=The font used for text in the Copilot Chat view -page.preferencesPage.autoApprove.name=Tool Auto Approve \ No newline at end of file +page.preferencesPage.autoApprove.name=Tool Auto Approve diff --git a/com.microsoft.copilot.eclipse.ui/plugin.xml b/com.microsoft.copilot.eclipse.ui/plugin.xml index 8771f7ac4..0b404d41f 100644 --- a/com.microsoft.copilot.eclipse.ui/plugin.xml +++ b/com.microsoft.copilot.eclipse.ui/plugin.xml @@ -7,6 +7,14 @@ class="com.microsoft.copilot.eclipse.ui.StartUp"> + + + + openChat; + + CopilotQuickFixProposal(String prompt) { + this(prompt, QuickFixProcessorSupport::openChat); + } + + CopilotQuickFixProposal(String prompt, Consumer openChat) { + this.prompt = prompt; + this.openChat = openChat; + } + + @Override + public void apply(IDocument document) { + openChat.accept(prompt); + } + + @Override + public Point getSelection(IDocument document) { + return null; + } + + @Override + public String getAdditionalProposalInfo() { + return null; + } + + @Override + public String getDisplayString() { + return Messages.quickFix_fixWithCopilot; + } + + @Override + public Image getImage() { + return CopilotImages.getImage(CopilotImages.IMG_GITHUB_COPILOT); + } + + @Override + public IContextInformation getContextInformation() { + return null; + } +} diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/quickfix/JavaCopilotQuickAssistProcessor.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/quickfix/JavaCopilotQuickAssistProcessor.java new file mode 100644 index 000000000..992c62673 --- /dev/null +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/quickfix/JavaCopilotQuickAssistProcessor.java @@ -0,0 +1,81 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +package com.microsoft.copilot.eclipse.ui.quickfix; + +import java.util.List; +import java.util.function.BooleanSupplier; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.jdt.core.ICompilationUnit; +import org.eclipse.jdt.ui.text.java.IInvocationContext; +import org.eclipse.jdt.ui.text.java.IJavaCompletionProposal; +import org.eclipse.jdt.ui.text.java.IProblemLocation; +import org.eclipse.jdt.ui.text.java.IQuickAssistProcessor; +import org.eclipse.jface.text.Document; +import org.eclipse.jface.text.IDocument; + +/** + * Provides a GitHub Copilot quick fix for problem markers in the Java editor. + */ +public class JavaCopilotQuickAssistProcessor implements IQuickAssistProcessor { + private final BooleanSupplier isCopilotAvailable; + + /** + * Creates a Java quick assist processor. + */ + public JavaCopilotQuickAssistProcessor() { + this(QuickFixProcessorSupport::isCopilotAvailable); + } + + JavaCopilotQuickAssistProcessor(BooleanSupplier isCopilotAvailable) { + this.isCopilotAvailable = isCopilotAvailable; + } + + @Override + public boolean hasAssists(IInvocationContext context) throws CoreException { + return createProposal(context) != null; + } + + @Override + public IJavaCompletionProposal[] getAssists(IInvocationContext context, IProblemLocation[] locations) + throws CoreException { + IJavaCompletionProposal proposal = createProposal(context); + return proposal == null ? null : new IJavaCompletionProposal[] { proposal }; + } + + private IJavaCompletionProposal createProposal(IInvocationContext context) throws CoreException { + if (!isCopilotAvailable.getAsBoolean() || context == null) { + return null; + } + + ICompilationUnit compilationUnit = context.getCompilationUnit(); + IResource resource = compilationUnit == null ? null : compilationUnit.getResource(); + if (!(resource instanceof IFile file)) { + return null; + } + + IDocument document = new Document(compilationUnit.getBuffer().getContents()); + List messages = QuickFixProcessorSupport.findProblemMessages(file, document, context.getSelectionOffset(), + context.getSelectionLength()); + if (messages.isEmpty()) { + return null; + } + return new JavaCopilotQuickFixProposal(QuickFixProcessorSupport.buildPrompt(messages)); + } + + private static final class JavaCopilotQuickFixProposal extends CopilotQuickFixProposal + implements IJavaCompletionProposal { + + private JavaCopilotQuickFixProposal(String prompt) { + super(prompt); + } + + @Override + public int getRelevance() { + return 0; + } + } +} diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/quickfix/QuickFixProcessorSupport.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/quickfix/QuickFixProcessorSupport.java new file mode 100644 index 000000000..d2de93b77 --- /dev/null +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/quickfix/QuickFixProcessorSupport.java @@ -0,0 +1,138 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +package com.microsoft.copilot.eclipse.ui.quickfix; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IMarker; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.jface.text.BadLocationException; +import org.eclipse.jface.text.IDocument; + +import com.microsoft.copilot.eclipse.core.AuthStatusManager; +import com.microsoft.copilot.eclipse.core.CopilotCore; +import com.microsoft.copilot.eclipse.ui.UiConstants; +import com.microsoft.copilot.eclipse.ui.i18n.Messages; +import com.microsoft.copilot.eclipse.ui.utils.UiUtils; + +final class QuickFixProcessorSupport { + + private QuickFixProcessorSupport() { + } + + static boolean isCopilotAvailable() { + CopilotCore plugin = CopilotCore.getPlugin(); + if (plugin == null) { + return false; + } + + AuthStatusManager authStatusManager = plugin.getAuthStatusManager(); + return authStatusManager != null && authStatusManager.isSignedIn(); + } + + static List findProblemMessages(IFile file, IDocument document, int offset, int length) + throws CoreException { + if (file == null || document == null || offset < 0 || offset > document.getLength()) { + return List.of(); + } + + int selectionLength = Math.max(0, length); + List problems = new ArrayList<>(); + for (IMarker marker : file.findMarkers(IMarker.PROBLEM, true, IResource.DEPTH_ZERO)) { + String message = marker.getAttribute(IMarker.MESSAGE, "").trim(); + if (message.isEmpty() || !overlaps(marker, document, offset, selectionLength)) { + continue; + } + + problems.add(new ProblemMarker(sortOffset(marker, document), message)); + } + + problems.sort(Comparator.comparingInt(ProblemMarker::start).thenComparing(ProblemMarker::message)); + LinkedHashSet uniqueMessages = new LinkedHashSet<>(); + for (ProblemMarker problem : problems) { + uniqueMessages.add(problem.message()); + } + return List.copyOf(uniqueMessages); + } + + private static boolean overlaps(IMarker marker, IDocument document, int offset, int selectionLength) + throws CoreException { + int markerStart = marker.getAttribute(IMarker.CHAR_START, -1); + int markerEnd = marker.getAttribute(IMarker.CHAR_END, -1); + if (markerStart >= 0 && markerEnd >= markerStart) { + if (selectionLength == 0) { + return markerStart == markerEnd ? offset == markerStart : markerStart <= offset && offset < markerEnd; + } + + long selectionEnd = (long) offset + selectionLength; + if (markerStart == markerEnd) { + return offset <= markerStart && markerStart < selectionEnd; + } + return markerStart < selectionEnd && offset < markerEnd; + } + + int markerLine = marker.getAttribute(IMarker.LINE_NUMBER, -1); + if (markerLine < 1) { + return false; + } + + try { + int selectionStartLine = document.getLineOfOffset(offset) + 1; + long requestedEndOffset = (long) offset + selectionLength - 1; + int selectionEndOffset = selectionLength == 0 ? offset + : (int) Math.min(document.getLength(), requestedEndOffset); + int selectionEndLine = document.getLineOfOffset(selectionEndOffset) + 1; + return selectionStartLine <= markerLine && markerLine <= selectionEndLine; + } catch (BadLocationException e) { + return false; + } + } + + private static int sortOffset(IMarker marker, IDocument document) throws CoreException { + int markerStart = marker.getAttribute(IMarker.CHAR_START, -1); + if (markerStart >= 0) { + return markerStart; + } + + int markerLine = marker.getAttribute(IMarker.LINE_NUMBER, -1); + if (markerLine < 1) { + return Integer.MAX_VALUE; + } + + try { + return document.getLineOffset(markerLine - 1); + } catch (BadLocationException e) { + return Integer.MAX_VALUE; + } + } + + static String buildPrompt(List messages) { + StringBuilder prompt = new StringBuilder(Messages.quickFix_prompt).append(System.lineSeparator()); + for (String message : messages) { + prompt.append(System.lineSeparator()).append("- ").append(message); + } + return prompt.toString(); + } + + static void openChat(String prompt) { + UiUtils.executeCommandWithParameters(UiConstants.OPEN_CHAT_VIEW_COMMAND_ID, createOpenChatParameters(prompt)); + } + + static Map createOpenChatParameters(String prompt) { + Map parameters = new HashMap<>(); + parameters.put(UiConstants.OPEN_CHAT_VIEW_INPUT_VALUE, prompt); + parameters.put(UiConstants.OPEN_CHAT_VIEW_AUTO_SEND, Boolean.FALSE.toString()); + return parameters; + } + + private record ProblemMarker(int start, String message) { + } +} From 4877ace83dd12ad2dcb51b20646c6326479e3bb5 Mon Sep 17 00:00:00 2001 From: lytreallynb Date: Thu, 13 Aug 2026 12:55:29 -0400 Subject: [PATCH 2/2] fix: preserve quick assist diagnostic context --- .../quickfix/QuickAssistExtensionTests.java | 17 ++----- .../QuickFixProcessorSupportTests.java | 44 ++++++++++++------ .../ui/quickfix/CopilotQuickFixProposal.java | 12 +++-- .../JavaCopilotQuickAssistProcessor.java | 13 +++--- .../ui/quickfix/QuickFixProcessorSupport.java | 46 +++++++++++++++++-- 5 files changed, 89 insertions(+), 43 deletions(-) diff --git a/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/quickfix/QuickAssistExtensionTests.java b/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/quickfix/QuickAssistExtensionTests.java index 0aa1c8b99..fee43279a 100644 --- a/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/quickfix/QuickAssistExtensionTests.java +++ b/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/quickfix/QuickAssistExtensionTests.java @@ -3,10 +3,7 @@ package com.microsoft.copilot.eclipse.ui.quickfix; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; import java.util.Arrays; @@ -17,19 +14,11 @@ class QuickAssistExtensionTests { @Test - void registersJavaQuickAssistProcessor() throws Exception { + void registersJavaQuickAssistProcessor() { IConfigurationElement element = findProcessor("org.eclipse.jdt.ui.quickAssistProcessors", - JavaCopilotQuickAssistProcessor.class.getName()); + "com.microsoft.copilot.eclipse.ui.quickfix.JavaCopilotQuickAssistProcessor"); - assertInstanceOf(JavaCopilotQuickAssistProcessor.class, element.createExecutableExtension("class")); - } - - @Test - void hidesProposalWhenCopilotIsUnavailable() throws Exception { - JavaCopilotQuickAssistProcessor processor = new JavaCopilotQuickAssistProcessor(() -> false); - - assertFalse(processor.hasAssists(null)); - assertNull(processor.getAssists(null, null)); + assertNotNull(element); } private IConfigurationElement findProcessor(String extensionPointId, String className) { diff --git a/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/quickfix/QuickFixProcessorSupportTests.java b/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/quickfix/QuickFixProcessorSupportTests.java index eaa1ace9e..4967cc962 100644 --- a/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/quickfix/QuickFixProcessorSupportTests.java +++ b/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/quickfix/QuickFixProcessorSupportTests.java @@ -18,6 +18,7 @@ import org.eclipse.core.resources.ResourcesPlugin; import org.eclipse.jface.text.Document; import org.eclipse.jface.text.IDocument; +import org.eclipse.swt.graphics.Point; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -58,9 +59,9 @@ void findsProblemAtCaret() throws Exception { createMarker("Fix the second line", 11, 25, 2); assertEquals(List.of("Fix the second line"), - QuickFixProcessorSupport.findProblemMessages(file, document, 18, 0)); - assertTrue(QuickFixProcessorSupport.findProblemMessages(file, document, 10, 0).isEmpty()); - assertTrue(QuickFixProcessorSupport.findProblemMessages(file, document, 25, 0).isEmpty()); + QuickFixProcessorSupport.findProblemContext(file, document, 18, 0).messages()); + assertTrue(QuickFixProcessorSupport.findProblemContext(file, document, 10, 0).messages().isEmpty()); + assertTrue(QuickFixProcessorSupport.findProblemContext(file, document, 25, 0).messages().isEmpty()); } @Test @@ -70,7 +71,21 @@ void findsAllProblemsOverlappingSelectionInSourceOrder() throws Exception { createMarker("Second problem", 15, 20, 2); assertEquals(List.of("Second problem", "Third problem"), - QuickFixProcessorSupport.findProblemMessages(file, document, 11, 28)); + QuickFixProcessorSupport.findProblemContext(file, document, 11, 28).messages()); + } + + @Test + void combinesProblemsAtSameRangeAndSelectsAffectedRange() throws Exception { + createMarker("Second problem", 11, 25, 2); + createMarker("Another problem", 11, 25, 2); + createMarker("Second problem", 11, 25, 2); + + QuickFixProcessorSupport.ProblemContext problemContext = + QuickFixProcessorSupport.findProblemContext(file, document, 18, 0); + + assertEquals(List.of("Another problem", "Second problem"), problemContext.messages()); + assertEquals(11, problemContext.selectionOffset()); + assertEquals(14, problemContext.selectionLength()); } @Test @@ -81,9 +96,9 @@ void supportsZeroLengthAndLineOnlyMarkers() throws Exception { lineMarker.setAttribute(IMarker.LINE_NUMBER, 3); assertEquals(List.of("Insertion problem"), - QuickFixProcessorSupport.findProblemMessages(file, document, 11, 0)); + QuickFixProcessorSupport.findProblemContext(file, document, 11, 0).messages()); assertEquals(List.of("Line problem"), - QuickFixProcessorSupport.findProblemMessages(file, document, 30, 0)); + QuickFixProcessorSupport.findProblemContext(file, document, 30, 0).messages()); } @Test @@ -94,7 +109,7 @@ void ordersLineOnlyAndCharacterRangeMarkersBySourcePosition() throws Exception { lineMarker.setAttribute(IMarker.LINE_NUMBER, 2); assertEquals(List.of("Second problem", "Third problem"), - QuickFixProcessorSupport.findProblemMessages(file, document, 11, 28)); + QuickFixProcessorSupport.findProblemContext(file, document, 11, 28).messages()); } @Test @@ -103,19 +118,19 @@ void ignoresMarkersWithoutMessageAndInvalidInvocationOffsets() throws Exception marker.setAttribute(IMarker.CHAR_START, 0); marker.setAttribute(IMarker.CHAR_END, 5); - assertTrue(QuickFixProcessorSupport.findProblemMessages(file, document, 2, 0).isEmpty()); - assertTrue(QuickFixProcessorSupport.findProblemMessages(file, document, -1, 0).isEmpty()); - assertTrue(QuickFixProcessorSupport.findProblemMessages(file, document, document.getLength() + 1, 0) - .isEmpty()); + assertTrue(QuickFixProcessorSupport.findProblemContext(file, document, 2, 0).messages().isEmpty()); + assertTrue(QuickFixProcessorSupport.findProblemContext(file, document, -1, 0).messages().isEmpty()); + assertTrue(QuickFixProcessorSupport.findProblemContext(file, document, document.getLength() + 1, 0) + .messages().isEmpty()); } @Test void handlesInvalidAndVeryLargeSelectionLengths() throws Exception { createMarker("Second problem", 11, 25, 2); - assertTrue(QuickFixProcessorSupport.findProblemMessages(file, document, 10, -1).isEmpty()); + assertTrue(QuickFixProcessorSupport.findProblemContext(file, document, 10, -1).messages().isEmpty()); assertEquals(List.of("Second problem"), - QuickFixProcessorSupport.findProblemMessages(file, document, 10, Integer.MAX_VALUE)); + QuickFixProcessorSupport.findProblemContext(file, document, 10, Integer.MAX_VALUE).messages()); } @Test @@ -127,11 +142,12 @@ void buildsPromptAndProposalPrefillsIt() { assertEquals(expectedPrompt, prompt); AtomicReference openedPrompt = new AtomicReference<>(); - CopilotQuickFixProposal proposal = new CopilotQuickFixProposal(prompt, openedPrompt::set); + CopilotQuickFixProposal proposal = new CopilotQuickFixProposal(prompt, 11, 14, openedPrompt::set); proposal.apply(document); assertEquals(expectedPrompt, openedPrompt.get()); assertEquals(Messages.quickFix_fixWithCopilot, proposal.getDisplayString()); + assertEquals(new Point(11, 14), proposal.getSelection(document)); Map parameters = QuickFixProcessorSupport.createOpenChatParameters(prompt); assertEquals(expectedPrompt, parameters.get(UiConstants.OPEN_CHAT_VIEW_INPUT_VALUE)); diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/quickfix/CopilotQuickFixProposal.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/quickfix/CopilotQuickFixProposal.java index 0d00f645c..ad0d8697b 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/quickfix/CopilotQuickFixProposal.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/quickfix/CopilotQuickFixProposal.java @@ -16,14 +16,18 @@ class CopilotQuickFixProposal implements ICompletionProposal { private final String prompt; + private final int selectionOffset; + private final int selectionLength; private final Consumer openChat; - CopilotQuickFixProposal(String prompt) { - this(prompt, QuickFixProcessorSupport::openChat); + CopilotQuickFixProposal(String prompt, int selectionOffset, int selectionLength) { + this(prompt, selectionOffset, selectionLength, QuickFixProcessorSupport::openChat); } - CopilotQuickFixProposal(String prompt, Consumer openChat) { + CopilotQuickFixProposal(String prompt, int selectionOffset, int selectionLength, Consumer openChat) { this.prompt = prompt; + this.selectionOffset = selectionOffset; + this.selectionLength = selectionLength; this.openChat = openChat; } @@ -34,7 +38,7 @@ public void apply(IDocument document) { @Override public Point getSelection(IDocument document) { - return null; + return new Point(selectionOffset, selectionLength); } @Override diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/quickfix/JavaCopilotQuickAssistProcessor.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/quickfix/JavaCopilotQuickAssistProcessor.java index 992c62673..cd111a5a3 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/quickfix/JavaCopilotQuickAssistProcessor.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/quickfix/JavaCopilotQuickAssistProcessor.java @@ -3,7 +3,6 @@ package com.microsoft.copilot.eclipse.ui.quickfix; -import java.util.List; import java.util.function.BooleanSupplier; import org.eclipse.core.resources.IFile; @@ -58,19 +57,21 @@ private IJavaCompletionProposal createProposal(IInvocationContext context) throw } IDocument document = new Document(compilationUnit.getBuffer().getContents()); - List messages = QuickFixProcessorSupport.findProblemMessages(file, document, context.getSelectionOffset(), + QuickFixProcessorSupport.ProblemContext problemContext = QuickFixProcessorSupport.findProblemContext(file, + document, context.getSelectionOffset(), context.getSelectionLength()); - if (messages.isEmpty()) { + if (problemContext.messages().isEmpty()) { return null; } - return new JavaCopilotQuickFixProposal(QuickFixProcessorSupport.buildPrompt(messages)); + return new JavaCopilotQuickFixProposal(QuickFixProcessorSupport.buildPrompt(problemContext.messages()), + problemContext.selectionOffset(), problemContext.selectionLength()); } private static final class JavaCopilotQuickFixProposal extends CopilotQuickFixProposal implements IJavaCompletionProposal { - private JavaCopilotQuickFixProposal(String prompt) { - super(prompt); + private JavaCopilotQuickFixProposal(String prompt, int selectionOffset, int selectionLength) { + super(prompt, selectionOffset, selectionLength); } @Override diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/quickfix/QuickFixProcessorSupport.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/quickfix/QuickFixProcessorSupport.java index d2de93b77..feadae142 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/quickfix/QuickFixProcessorSupport.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/quickfix/QuickFixProcessorSupport.java @@ -38,10 +38,10 @@ static boolean isCopilotAvailable() { return authStatusManager != null && authStatusManager.isSignedIn(); } - static List findProblemMessages(IFile file, IDocument document, int offset, int length) + static ProblemContext findProblemContext(IFile file, IDocument document, int offset, int length) throws CoreException { if (file == null || document == null || offset < 0 || offset > document.getLength()) { - return List.of(); + return ProblemContext.empty(); } int selectionLength = Math.max(0, length); @@ -52,7 +52,8 @@ static List findProblemMessages(IFile file, IDocument document, int offs continue; } - problems.add(new ProblemMarker(sortOffset(marker, document), message)); + int start = sortOffset(marker, document); + problems.add(new ProblemMarker(start, selectionEnd(marker, document, start), message)); } problems.sort(Comparator.comparingInt(ProblemMarker::start).thenComparing(ProblemMarker::message)); @@ -60,7 +61,17 @@ static List findProblemMessages(IFile file, IDocument document, int offs for (ProblemMarker problem : problems) { uniqueMessages.add(problem.message()); } - return List.copyOf(uniqueMessages); + if (uniqueMessages.isEmpty()) { + return ProblemContext.empty(); + } + + int contextStart = offset; + int contextEnd = (int) Math.min(document.getLength(), (long) offset + selectionLength); + if (selectionLength == 0) { + contextStart = problems.stream().mapToInt(ProblemMarker::start).min().orElse(offset); + contextEnd = problems.stream().mapToInt(ProblemMarker::end).max().orElse(contextStart); + } + return new ProblemContext(List.copyOf(uniqueMessages), contextStart, Math.max(0, contextEnd - contextStart)); } private static boolean overlaps(IMarker marker, IDocument document, int offset, int selectionLength) @@ -114,6 +125,25 @@ private static int sortOffset(IMarker marker, IDocument document) throws CoreExc } } + private static int selectionEnd(IMarker marker, IDocument document, int start) throws CoreException { + int markerStart = marker.getAttribute(IMarker.CHAR_START, -1); + int markerEnd = marker.getAttribute(IMarker.CHAR_END, -1); + if (markerStart >= 0 && markerEnd >= markerStart) { + return Math.min(document.getLength(), markerEnd); + } + + int markerLine = marker.getAttribute(IMarker.LINE_NUMBER, -1); + if (markerLine < 1) { + return start; + } + + try { + return start + document.getLineInformation(markerLine - 1).getLength(); + } catch (BadLocationException e) { + return start; + } + } + static String buildPrompt(List messages) { StringBuilder prompt = new StringBuilder(Messages.quickFix_prompt).append(System.lineSeparator()); for (String message : messages) { @@ -133,6 +163,12 @@ static Map createOpenChatParameters(String prompt) { return parameters; } - private record ProblemMarker(int start, String message) { + record ProblemContext(List messages, int selectionOffset, int selectionLength) { + private static ProblemContext empty() { + return new ProblemContext(List.of(), 0, 0); + } + } + + private record ProblemMarker(int start, int end, String message) { } }