From 46e49a1237aa613fd6484cb9e1344d53dd6cae61 Mon Sep 17 00:00:00 2001 From: Nanook Date: Fri, 5 Jun 2026 20:59:05 +0000 Subject: [PATCH 1/2] fix: clear closed editor file context --- .../services/ReferencedFileServiceTest.java | 126 ++++++++++++++++++ .../chat/services/ReferencedFileService.java | 40 +++++- 2 files changed, 159 insertions(+), 7 deletions(-) create mode 100644 com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/chat/services/ReferencedFileServiceTest.java diff --git a/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/chat/services/ReferencedFileServiceTest.java b/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/chat/services/ReferencedFileServiceTest.java new file mode 100644 index 00000000..1a2a0d3a --- /dev/null +++ b/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/chat/services/ReferencedFileServiceTest.java @@ -0,0 +1,126 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +package com.microsoft.copilot.eclipse.ui.chat.services; + +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +import org.eclipse.core.resources.IFile; +import org.eclipse.swt.widgets.Display; +import org.eclipse.ui.IEditorPart; +import org.eclipse.ui.IEditorReference; +import org.eclipse.ui.IPartListener2; +import org.eclipse.ui.IPartService; +import org.eclipse.ui.IWorkbench; +import org.eclipse.ui.IWorkbenchPage; +import org.eclipse.ui.IWorkbenchPartReference; +import org.eclipse.ui.IWorkbenchWindow; +import org.eclipse.ui.PlatformUI; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.junit.jupiter.MockitoExtension; + +import com.microsoft.copilot.eclipse.ui.utils.UiUtils; + +@ExtendWith(MockitoExtension.class) +class ReferencedFileServiceTest { + + @Mock + private IWorkbench workbench; + + @Mock + private IWorkbenchWindow window; + + @Mock + private IPartService partService; + + @Mock + private IWorkbenchPage activePage; + + @Mock + private IEditorReference remainingEditorReference; + + @Mock + private IWorkbenchPartReference closedPartReference; + + @Mock + private IEditorPart closedEditor; + + @Mock + private IFile currentFile; + + @Test + void partClosed_WhenClosedEditorIsCurrentFileAndEditorReferencesRemain_ShouldClearCurrentFile() + throws Exception { + try (MockedStatic platformUi = mockStatic(PlatformUI.class); + MockedStatic uiUtils = mockStatic(UiUtils.class)) { + platformUi.when(PlatformUI::getWorkbench).thenReturn(workbench); + when(workbench.getWorkbenchWindows()).thenReturn(new IWorkbenchWindow[] { window }); + when(window.getPartService()).thenReturn(partService); + + ReferencedFileService service = new ReferencedFileService(); + try { + IPartListener2 listener = getRegisteredPartListener(); + when(closedPartReference.getPart(false)).thenReturn(closedEditor); + uiUtils.when(() -> UiUtils.getFileFromEditorPart(closedEditor)).thenReturn(currentFile); + uiUtils.when(UiUtils::getActivePage).thenReturn(activePage); + when(activePage.getEditorReferences()).thenReturn(new IEditorReference[] { remainingEditorReference }); + + setCurrentFile(service, currentFile); + assertSame(currentFile, service.getCurrentFile()); + + listener.partClosed(closedPartReference); + + assertNull(service.getCurrentFile()); + } finally { + service.dispose(); + } + } + } + + private IPartListener2 getRegisteredPartListener() { + ArgumentCaptor listenerCaptor = ArgumentCaptor.forClass(IPartListener2.class); + verify(partService).addPartListener(listenerCaptor.capture()); + return listenerCaptor.getValue(); + } + + private static void setCurrentFile(ReferencedFileService service, IFile file) throws Exception { + Object currentFileObservable = getField(service, "currentFileObservable"); + runOnDisplayThread(() -> invokeSetValue(currentFileObservable, file)); + } + + private static Object getField(Object target, String fieldName) throws NoSuchFieldException, + IllegalAccessException { + Field field = target.getClass().getDeclaredField(fieldName); + field.setAccessible(true); + return field.get(target); + } + + private static void runOnDisplayThread(Runnable runnable) { + if (Display.getCurrent() != null) { + runnable.run(); + return; + } + Display.getDefault().syncExec(runnable); + } + + private static void invokeSetValue(Object observable, Object value) { + try { + Method setValue = observable.getClass().getMethod("setValue", Object.class); + setValue.invoke(observable, value); + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { + throw new IllegalStateException("Failed to set observable value", e); + } + } +} diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/services/ReferencedFileService.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/services/ReferencedFileService.java index e447b648..14b497b7 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/services/ReferencedFileService.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/services/ReferencedFileService.java @@ -105,12 +105,10 @@ public void partBroughtToTop(IWorkbenchPartReference partRef) { @Override public void partClosed(IWorkbenchPartReference partRef) { - IWorkbenchPage page = UiUtils.getActivePage(); - if (page == null || page.getEditorReferences().length == 0) { - ensureRealm(() -> { - currentFileObservable.setValue(null); - currentSelectionObservable.setValue(null); - }); + boolean closedCurrentFile = isCurrentReferencedFile(partRef); + boolean noOpenEditors = hasNoOpenEditors(); + if (closedCurrentFile || noOpenEditors) { + clearCurrentReferencedFile(); } untrackSelectionInEditor(partRef); } @@ -479,6 +477,34 @@ private void updateCurrentReferencedFile(IWorkbenchPartReference partRef) { } } + private boolean isCurrentReferencedFile(IWorkbenchPartReference partRef) { + IWorkbenchPart part = partRef.getPart(false); + if (!(part instanceof IEditorPart editorPart)) { + return false; + } + + IFile closedFile = UiUtils.getFileFromEditorPart(editorPart); + if (closedFile == null) { + return false; + } + + AtomicReference currentFile = new AtomicReference<>(); + ensureRealm(() -> currentFile.set(currentFileObservable.getValue())); + return closedFile.equals(currentFile.get()); + } + + private boolean hasNoOpenEditors() { + IWorkbenchPage page = UiUtils.getActivePage(); + return page == null || page.getEditorReferences().length == 0; + } + + private void clearCurrentReferencedFile() { + ensureRealm(() -> { + currentFileObservable.setValue(null); + currentSelectionObservable.setValue(null); + }); + } + private void updateCurrentReferencedFile(IEditorPart editorPart) { if (editorPart == null) { updateObservable(currentFileObservable, null); @@ -529,4 +555,4 @@ private void unregisterPartListener() { } } -} \ No newline at end of file +} From 122edf0dd5df779fcff8f327acd52c2faf906850 Mon Sep 17 00:00:00 2001 From: Nanook Date: Thu, 25 Jun 2026 04:31:24 +0000 Subject: [PATCH 2/2] fix: handle closed editor reference cleanup --- .../services/ReferencedFileServiceTest.java | 118 ++++++++++++------ .../chat/services/ReferencedFileService.java | 86 +++++++++++-- 2 files changed, 158 insertions(+), 46 deletions(-) diff --git a/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/chat/services/ReferencedFileServiceTest.java b/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/chat/services/ReferencedFileServiceTest.java index 1a2a0d3a..a1c5093d 100644 --- a/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/chat/services/ReferencedFileServiceTest.java +++ b/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/chat/services/ReferencedFileServiceTest.java @@ -9,19 +9,15 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import java.lang.reflect.Field; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; - import org.eclipse.core.resources.IFile; -import org.eclipse.swt.widgets.Display; +import org.eclipse.ui.IEditorInput; import org.eclipse.ui.IEditorPart; import org.eclipse.ui.IEditorReference; +import org.eclipse.ui.IFileEditorInput; import org.eclipse.ui.IPartListener2; import org.eclipse.ui.IPartService; import org.eclipse.ui.IWorkbench; import org.eclipse.ui.IWorkbenchPage; -import org.eclipse.ui.IWorkbenchPartReference; import org.eclipse.ui.IWorkbenchWindow; import org.eclipse.ui.PlatformUI; import org.junit.jupiter.api.Test; @@ -49,14 +45,23 @@ class ReferencedFileServiceTest { private IWorkbenchPage activePage; @Mock - private IEditorReference remainingEditorReference; + private IWorkbenchPage otherPage; @Mock - private IWorkbenchPartReference closedPartReference; + private IEditorReference closedEditorReference; + + @Mock + private IEditorReference remainingEditorReference; @Mock private IEditorPart closedEditor; + @Mock + private IFileEditorInput closedEditorInput; + + @Mock + private IEditorInput nonFileEditorInput; + @Mock private IFile currentFile; @@ -69,18 +74,19 @@ void partClosed_WhenClosedEditorIsCurrentFileAndEditorReferencesRemain_ShouldCle when(workbench.getWorkbenchWindows()).thenReturn(new IWorkbenchWindow[] { window }); when(window.getPartService()).thenReturn(partService); - ReferencedFileService service = new ReferencedFileService(); + TestReferencedFileService service = new TestReferencedFileService(); try { IPartListener2 listener = getRegisteredPartListener(); - when(closedPartReference.getPart(false)).thenReturn(closedEditor); + when(closedEditorReference.getEditorInput()).thenReturn(nonFileEditorInput); + when(closedEditorReference.getPart(false)).thenReturn(closedEditor); uiUtils.when(() -> UiUtils.getFileFromEditorPart(closedEditor)).thenReturn(currentFile); - uiUtils.when(UiUtils::getActivePage).thenReturn(activePage); + when(window.getPages()).thenReturn(new IWorkbenchPage[] { activePage }); when(activePage.getEditorReferences()).thenReturn(new IEditorReference[] { remainingEditorReference }); - setCurrentFile(service, currentFile); + service.setCurrentFile(currentFile); assertSame(currentFile, service.getCurrentFile()); - listener.partClosed(closedPartReference); + listener.partClosed(closedEditorReference); assertNull(service.getCurrentFile()); } finally { @@ -89,38 +95,74 @@ void partClosed_WhenClosedEditorIsCurrentFileAndEditorReferencesRemain_ShouldCle } } - private IPartListener2 getRegisteredPartListener() { - ArgumentCaptor listenerCaptor = ArgumentCaptor.forClass(IPartListener2.class); - verify(partService).addPartListener(listenerCaptor.capture()); - return listenerCaptor.getValue(); - } + @Test + void partClosed_WhenEditorPartIsDisposedButReferenceInputIsCurrentFile_ShouldClearCurrentFile() + throws Exception { + try (MockedStatic platformUi = mockStatic(PlatformUI.class)) { + platformUi.when(PlatformUI::getWorkbench).thenReturn(workbench); + when(workbench.getWorkbenchWindows()).thenReturn(new IWorkbenchWindow[] { window }); + when(window.getPartService()).thenReturn(partService); - private static void setCurrentFile(ReferencedFileService service, IFile file) throws Exception { - Object currentFileObservable = getField(service, "currentFileObservable"); - runOnDisplayThread(() -> invokeSetValue(currentFileObservable, file)); - } + TestReferencedFileService service = new TestReferencedFileService(); + try { + IPartListener2 listener = getRegisteredPartListener(); + when(closedEditorReference.getEditorInput()).thenReturn(closedEditorInput); + when(closedEditorInput.getFile()).thenReturn(currentFile); + when(window.getPages()).thenReturn(new IWorkbenchPage[] { activePage }); + when(activePage.getEditorReferences()).thenReturn(new IEditorReference[] { remainingEditorReference }); + + service.setCurrentFile(currentFile); + assertSame(currentFile, service.getCurrentFile()); + + listener.partClosed(closedEditorReference); - private static Object getField(Object target, String fieldName) throws NoSuchFieldException, - IllegalAccessException { - Field field = target.getClass().getDeclaredField(fieldName); - field.setAccessible(true); - return field.get(target); + assertNull(service.getCurrentFile()); + } finally { + service.dispose(); + } + } } - private static void runOnDisplayThread(Runnable runnable) { - if (Display.getCurrent() != null) { - runnable.run(); - return; + @Test + void partClosed_WhenActivePageHasNoEditorsButAnotherPageHasEditors_ShouldKeepCurrentFile() + throws Exception { + try (MockedStatic platformUi = mockStatic(PlatformUI.class); + MockedStatic uiUtils = mockStatic(UiUtils.class)) { + platformUi.when(PlatformUI::getWorkbench).thenReturn(workbench); + when(workbench.getWorkbenchWindows()).thenReturn(new IWorkbenchWindow[] { window }); + when(window.getPartService()).thenReturn(partService); + + TestReferencedFileService service = new TestReferencedFileService(); + try { + IPartListener2 listener = getRegisteredPartListener(); + when(closedEditorReference.getEditorInput()).thenReturn(nonFileEditorInput); + when(closedEditorReference.getPart(false)).thenReturn(closedEditor); + uiUtils.when(() -> UiUtils.getFileFromEditorPart(closedEditor)).thenReturn(null); + when(window.getPages()).thenReturn(new IWorkbenchPage[] { activePage, otherPage }); + when(activePage.getEditorReferences()).thenReturn(new IEditorReference[0]); + when(otherPage.getEditorReferences()).thenReturn(new IEditorReference[] { remainingEditorReference }); + + service.setCurrentFile(currentFile); + assertSame(currentFile, service.getCurrentFile()); + + listener.partClosed(closedEditorReference); + + assertSame(currentFile, service.getCurrentFile()); + } finally { + service.dispose(); + } } - Display.getDefault().syncExec(runnable); } - private static void invokeSetValue(Object observable, Object value) { - try { - Method setValue = observable.getClass().getMethod("setValue", Object.class); - setValue.invoke(observable, value); - } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { - throw new IllegalStateException("Failed to set observable value", e); + private IPartListener2 getRegisteredPartListener() { + ArgumentCaptor listenerCaptor = ArgumentCaptor.forClass(IPartListener2.class); + verify(partService).addPartListener(listenerCaptor.capture()); + return listenerCaptor.getValue(); + } + + private static class TestReferencedFileService extends ReferencedFileService { + void setCurrentFile(IFile file) { + setCurrentFileForTest(file); } } } diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/services/ReferencedFileService.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/services/ReferencedFileService.java index 14b497b7..8615b9ec 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/services/ReferencedFileService.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/services/ReferencedFileService.java @@ -26,13 +26,19 @@ import org.eclipse.lsp4e.LSPEclipseUtils; import org.eclipse.lsp4j.Position; import org.eclipse.lsp4j.Range; +import org.eclipse.ui.IEditorInput; import org.eclipse.ui.IEditorPart; +import org.eclipse.ui.IEditorReference; +import org.eclipse.ui.IFileEditorInput; import org.eclipse.ui.IPartListener2; +import org.eclipse.ui.IWorkbench; import org.eclipse.ui.IWorkbenchPage; import org.eclipse.ui.IWorkbenchPart; import org.eclipse.ui.IWorkbenchPartReference; import org.eclipse.ui.IWorkbenchWindow; +import org.eclipse.ui.PartInitException; import org.eclipse.ui.PlatformUI; +import org.eclipse.ui.ide.ResourceUtil; import org.eclipse.ui.texteditor.ITextEditor; import com.microsoft.copilot.eclipse.core.Constants; @@ -478,12 +484,7 @@ private void updateCurrentReferencedFile(IWorkbenchPartReference partRef) { } private boolean isCurrentReferencedFile(IWorkbenchPartReference partRef) { - IWorkbenchPart part = partRef.getPart(false); - if (!(part instanceof IEditorPart editorPart)) { - return false; - } - - IFile closedFile = UiUtils.getFileFromEditorPart(editorPart); + IFile closedFile = getFileFromPartReference(partRef); if (closedFile == null) { return false; } @@ -493,9 +494,71 @@ private boolean isCurrentReferencedFile(IWorkbenchPartReference partRef) { return closedFile.equals(currentFile.get()); } + private IFile getFileFromPartReference(IWorkbenchPartReference partRef) { + if (partRef instanceof IEditorReference editorReference) { + IFile file = getFileFromEditorReference(editorReference); + if (file != null) { + return file; + } + } + + IWorkbenchPart part = partRef.getPart(false); + if (part instanceof IEditorPart editorPart) { + return UiUtils.getFileFromEditorPart(editorPart); + } + return null; + } + + private IFile getFileFromEditorReference(IEditorReference editorReference) { + try { + return getFileFromEditorInput(editorReference.getEditorInput()); + } catch (PartInitException e) { + CopilotCore.LOGGER.error("Failed to get editor input from part reference", e); + return null; + } + } + + private IFile getFileFromEditorInput(IEditorInput input) { + if (input instanceof IFileEditorInput fileEditorInput) { + return fileEditorInput.getFile(); + } + return ResourceUtil.getFile(input); + } + private boolean hasNoOpenEditors() { - IWorkbenchPage page = UiUtils.getActivePage(); - return page == null || page.getEditorReferences().length == 0; + IWorkbench workbench = PlatformUI.getWorkbench(); + if (workbench == null) { + return true; + } + + IWorkbenchWindow[] windows = workbench.getWorkbenchWindows(); + if (windows == null || windows.length == 0) { + return true; + } + + for (IWorkbenchWindow window : windows) { + if (window == null) { + continue; + } + + IWorkbenchPage[] pages = window.getPages(); + if (pages == null || pages.length == 0) { + IWorkbenchPage activePage = window.getActivePage(); + pages = activePage == null ? new IWorkbenchPage[0] : new IWorkbenchPage[] { activePage }; + } + + for (IWorkbenchPage page : pages) { + if (page == null) { + continue; + } + + IEditorReference[] editorReferences = page.getEditorReferences(); + if (editorReferences != null && editorReferences.length > 0) { + return false; + } + } + } + return true; } private void clearCurrentReferencedFile() { @@ -505,6 +568,13 @@ private void clearCurrentReferencedFile() { }); } + /** + * Sets the current file for tests. + */ + protected void setCurrentFileForTest(IFile file) { + ensureRealm(() -> currentFileObservable.setValue(file)); + } + private void updateCurrentReferencedFile(IEditorPart editorPart) { if (editorPart == null) { updateObservable(currentFileObservable, null);