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..a1c5093d --- /dev/null +++ b/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/chat/services/ReferencedFileServiceTest.java @@ -0,0 +1,168 @@ +// 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 org.eclipse.core.resources.IFile; +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.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 IWorkbenchPage otherPage; + + @Mock + private IEditorReference closedEditorReference; + + @Mock + private IEditorReference remainingEditorReference; + + @Mock + private IEditorPart closedEditor; + + @Mock + private IFileEditorInput closedEditorInput; + + @Mock + private IEditorInput nonFileEditorInput; + + @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); + + 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(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); + + assertNull(service.getCurrentFile()); + } finally { + service.dispose(); + } + } + } + + @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); + + 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); + + assertNull(service.getCurrentFile()); + } finally { + service.dispose(); + } + } + } + + @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(); + } + } + } + + 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 e447b648..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; @@ -105,12 +111,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 +483,98 @@ private void updateCurrentReferencedFile(IWorkbenchPartReference partRef) { } } + private boolean isCurrentReferencedFile(IWorkbenchPartReference partRef) { + IFile closedFile = getFileFromPartReference(partRef); + if (closedFile == null) { + return false; + } + + AtomicReference currentFile = new AtomicReference<>(); + ensureRealm(() -> currentFile.set(currentFileObservable.getValue())); + 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() { + 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() { + ensureRealm(() -> { + currentFileObservable.setValue(null); + currentSelectionObservable.setValue(null); + }); + } + + /** + * 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); @@ -529,4 +625,4 @@ private void unregisterPartListener() { } } -} \ No newline at end of file +}