Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

package com.microsoft.copilot.eclipse.core;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.EnumSource;
import org.junit.jupiter.params.provider.NullSource;
import org.junit.jupiter.params.provider.ValueSource;

import com.microsoft.copilot.eclipse.core.Constants.CustomInstructionsChatLoadScope;

class CustomInstructionsChatLoadScopeTest {

@ParameterizedTest
@EnumSource(CustomInstructionsChatLoadScope.class)
void testStringToEnumEntryConversion(CustomInstructionsChatLoadScope enumEntry) {
String inputValue = enumEntry.getValue();

CustomInstructionsChatLoadScope actualResult = CustomInstructionsChatLoadScope.fromValue(inputValue);

assertEquals(enumEntry, actualResult);
}

@ParameterizedTest
@ValueSource(strings = { "wrongValue" })
@NullSource
void testStringToEnumEntryConversionThrowsExceptionForWrongValues(String value) {
assertThrows(IllegalArgumentException.class, () -> CustomInstructionsChatLoadScope.fromValue(value));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ private Constants() {
public static final String CUSTOM_INSTRUCTIONS_WORKSPACE = "customInstructionsWorkspace";
public static final String CUSTOM_INSTRUCTIONS_WORKSPACE_ENABLED = "customInstructionsWorkspaceEnabled";
public static final String CUSTOM_INSTRUCTIONS_GIT_COMMIT = "customInstructionsGitCommit";
public static final String CUSTOM_INSTRUCTIONS_CHAT_LOAD_SCOPE = "customInstructionsChatLoadScope";
private static final String CUSTOM_INSTRUCTIONS_CHAT_LOAD_SCOPE_ALL = "allProjects";
private static final String CUSTOM_INSTRUCTIONS_CHAT_LOAD_SCOPE_REFERENCED = "referencedProjects";
public static final String GITHUB_COPILOT_URL = "http://github.com";
@Deprecated
public static final String QUICK_START_VERSION = "quickStartVersion";
Expand Down Expand Up @@ -73,4 +76,52 @@ private Constants() {
.of(Stream.concat(Stream.concat(BASE_EXCLUDED_FILE_TYPES.stream(), ADDITIONAL_EXCLUDED_FILE_TYPES.stream()),
ALLOWED_IMAGE_EXTENSIONS.stream()).toArray(String[]::new));

/**
* Scope loading modes for custom instructions in GitHub Copilot chat.
* <ul>
* <li><b>ALL_PROJECTS</b>: Load custom instructions from all projects in the Eclipse workspace
* <li><b>REFERENCED_PROJECTS</b>: Load custom instructions only from parent projects of files/folders
* referenced in the Copilot chat
* </ul>
*/
public enum CustomInstructionsChatLoadScope {
ALL_PROJECTS(Constants.CUSTOM_INSTRUCTIONS_CHAT_LOAD_SCOPE_ALL),
REFERENCED_PROJECTS(Constants.CUSTOM_INSTRUCTIONS_CHAT_LOAD_SCOPE_REFERENCED);

public static final CustomInstructionsChatLoadScope DEFAULT_VALUE =
CustomInstructionsChatLoadScope.ALL_PROJECTS;

private final String value;

CustomInstructionsChatLoadScope(String value) {
this.value = value;
}

/**
* Returns the string value representing this enum entry for preference serialization.
*
* @return the string value for this preference setting
*/
public String getValue() {
return value;
}

/**
* Retrieves the enum constant corresponding to the given string value
* if available, otherwise an {@link IllegalArgumentException} is thrown.
*
* @param value the string value (preference) representing an enum entry
* @return the enum entry representing the given value
* @throws IllegalArgumentException if the value does not correspond to any enum entry
*/
public static CustomInstructionsChatLoadScope fromValue(String value) {
for (CustomInstructionsChatLoadScope scope : values()) {
if (scope.getValue().equals(value)) {
return scope;
}
}
throw new IllegalArgumentException("Unknown CustomInstructionsLoadScope value: " + value);
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import org.eclipse.lsp4j.ProgressParams;
import org.eclipse.lsp4j.Range;
import org.eclipse.lsp4j.TextDocumentIdentifier;
import org.eclipse.lsp4j.WorkspaceFolder;
import org.eclipse.lsp4j.jsonrpc.Endpoint;
import org.eclipse.lsp4j.jsonrpc.messages.Either;
import org.eclipse.lsp4j.services.LanguageServer;
Expand Down Expand Up @@ -275,6 +276,19 @@ public CompletableFuture<ChatCreateResult> createConversation(String workDoneTok
List<IResource> files, IFile currentFile, Range currentSelection, List<Turn> turns, CopilotModel activeModel,
String chatModeName, String customChatModeId, List<TodoItem> todos, String agentSlug,
String agentJobWorkspaceFolder) {
return createConversation(workDoneToken, message, files, currentFile, currentSelection, turns, activeModel,
chatModeName, customChatModeId, todos, agentSlug, agentJobWorkspaceFolder,
LSPEclipseUtils.getWorkspaceFolders());
}

/**
* Create a conversation with the given parameters, including optional workspace folders argument.
*/
public CompletableFuture<ChatCreateResult> createConversation(String workDoneToken, String message,
List<IResource> files, IFile currentFile, Range currentSelection, List<Turn> turns, CopilotModel activeModel,
String chatModeName, String customChatModeId, List<TodoItem> todos, String agentSlug,
String agentJobWorkspaceFolder, List<WorkspaceFolder> workspaceFolders) {

boolean supportVision = activeModel.getCapabilities().supports().vision();
Either<String, List<ChatCompletionContentPart>> messageWithImages = ChatMessageUtils
.createMessageWithImages(message, FileUtils.filterFilesFrom(files), supportVision);
Expand All @@ -288,7 +302,7 @@ public CompletableFuture<ChatCreateResult> createConversation(String workDoneTok

if (StringUtils.isBlank(agentSlug)) {
param.setWorkspaceFolder(PlatformUtils.getWorkspaceRootUri());
Comment thread
jdneo marked this conversation as resolved.
param.setWorkspaceFolders(LSPEclipseUtils.getWorkspaceFolders());
param.setWorkspaceFolders(workspaceFolders);
param.setTodoList(todos);
} else {
// Set agentSlug if provided - this will modify the first turn's agentSlug
Expand Down Expand Up @@ -333,6 +347,19 @@ public CompletableFuture<ChatTurnResult> addConversationTurn(String workDoneToke
String message, List<IResource> files, IFile currentFile, Range currentSelection, CopilotModel activeModel,
String chatModeName, String customChatModeId, List<TodoItem> todoList, String agentSlug,
String agentJobWorkspaceFolder) {
return addConversationTurn(workDoneToken, conversationId, message, files, currentFile, currentSelection,
activeModel, chatModeName, customChatModeId, todoList, agentSlug, agentJobWorkspaceFolder,
LSPEclipseUtils.getWorkspaceFolders());
}

/**
* Create a conversation turn with the given parameters, including optional workspace folders argument.
*/
public CompletableFuture<ChatTurnResult> addConversationTurn(String workDoneToken, String conversationId,
String message, List<IResource> files, IFile currentFile, Range currentSelection, CopilotModel activeModel,
String chatModeName, String customChatModeId, List<TodoItem> todoList, String agentSlug,
String agentJobWorkspaceFolder, List<WorkspaceFolder> workspaceFolders) {

boolean supportVision = activeModel.getCapabilities().supports().vision();
Either<String, List<ChatCompletionContentPart>> messageWithImages = ChatMessageUtils
.createMessageWithImages(message, FileUtils.filterFilesFrom(files), supportVision);
Expand All @@ -346,7 +373,7 @@ public CompletableFuture<ChatTurnResult> addConversationTurn(String workDoneToke

if (StringUtils.isBlank(agentSlug)) {
param.setWorkspaceFolder(PlatformUtils.getWorkspaceRootUri());
param.setWorkspaceFolders(LSPEclipseUtils.getWorkspaceFolders());
param.setWorkspaceFolders(workspaceFolders);
param.setTodoList(todoList);
} else {
param.setAgentSlug(agentSlug);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ Require-Bundle: org.mockito.mockito-core;bundle-version="5.14.2",
org.eclipse.lsp4e;bundle-version="0.18.1",
org.eclipse.jdt.annotation;resolution:=optional,
junit-jupiter-api;bundle-version="5.10.1",
junit-jupiter-params;bundle-version="5.10.1",
org.mockito.junit-jupiter;bundle-version="5.10.2",
com.microsoft.copilot.eclipse.core;bundle-version="0.15.0",
com.microsoft.copilot.eclipse.ui;bundle-version="0.15.0",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

package com.microsoft.copilot.eclipse.ui.utils;

import static org.junit.jupiter.api.Assertions.assertEquals;

import org.eclipse.jface.preference.IPreferenceStore;
import org.eclipse.jface.preference.PreferenceStore;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.EnumSource;

import com.microsoft.copilot.eclipse.core.Constants;
import com.microsoft.copilot.eclipse.core.Constants.CustomInstructionsChatLoadScope;

class PreferencesUtilsTest {

private IPreferenceStore store;

@BeforeEach
void setUp() {
store = new PreferenceStore();
store.setDefault(Constants.CUSTOM_INSTRUCTIONS_CHAT_LOAD_SCOPE,
CustomInstructionsChatLoadScope.DEFAULT_VALUE.getValue());
}

@AfterEach
void tearDown() {
store = null;
}

@ParameterizedTest
@EnumSource(CustomInstructionsChatLoadScope.class)
void testGetCustomInstructionsChatLoadScope_returnsStoredValue(CustomInstructionsChatLoadScope scope) {
store.setValue(Constants.CUSTOM_INSTRUCTIONS_CHAT_LOAD_SCOPE, scope.getValue());

CustomInstructionsChatLoadScope result = PreferencesUtils.getCustomInstructionsChatLoadScope(store);

assertEquals(scope, result);
}

@Test
void testGetCustomInstructionsChatLoadScope_fallsBackToDefaultInCaseOfInvalidValueInStore() {
store.setValue(Constants.CUSTOM_INSTRUCTIONS_CHAT_LOAD_SCOPE, "invalid_value");

CustomInstructionsChatLoadScope result = PreferencesUtils.getCustomInstructionsChatLoadScope(store);

assertEquals(CustomInstructionsChatLoadScope.DEFAULT_VALUE, result);
assertEquals(CustomInstructionsChatLoadScope.DEFAULT_VALUE.getValue(),
store.getString(Constants.CUSTOM_INSTRUCTIONS_CHAT_LOAD_SCOPE),
"The invalid stored value should have been corrected to the default");
}

@Test
void testGetCustomInstructionsChatLoadScopeDefault_returnsConfiguredDefault() {
// explicitly store varying values for InstanceScope and DefaultScope
store.setValue(Constants.CUSTOM_INSTRUCTIONS_CHAT_LOAD_SCOPE,
CustomInstructionsChatLoadScope.ALL_PROJECTS.getValue());
store.setDefault(Constants.CUSTOM_INSTRUCTIONS_CHAT_LOAD_SCOPE,
CustomInstructionsChatLoadScope.REFERENCED_PROJECTS.getValue());

CustomInstructionsChatLoadScope result = PreferencesUtils.getCustomInstructionsChatLoadScopeDefault(store);

// Should return the default value, not the stored value
assertEquals(CustomInstructionsChatLoadScope.REFERENCED_PROJECTS, result);
}

@Test
void testGetCustomInstructionsChatLoadScopeDefault_fallsBackToValidDefaultInCaseOfInvalidValue() {
String invalidDefaultValue = "invalid_default_value";
String instanceScopValue = CustomInstructionsChatLoadScope.REFERENCED_PROJECTS.getValue();
store.setDefault(Constants.CUSTOM_INSTRUCTIONS_CHAT_LOAD_SCOPE, invalidDefaultValue);
store.setValue(Constants.CUSTOM_INSTRUCTIONS_CHAT_LOAD_SCOPE, instanceScopValue);

CustomInstructionsChatLoadScope result = PreferencesUtils.getCustomInstructionsChatLoadScopeDefault(store);

assertEquals(CustomInstructionsChatLoadScope.DEFAULT_VALUE, result);
assertEquals(invalidDefaultValue,
store.getDefaultString(Constants.CUSTOM_INSTRUCTIONS_CHAT_LOAD_SCOPE),
"The stored DefaultScope value should not change, even if it's invalid. "
+ "That is repsonsibility of a PreferenceInitializer.");
assertEquals(instanceScopValue,
store.getString(Constants.CUSTOM_INSTRUCTIONS_CHAT_LOAD_SCOPE),
"The stored InstanceScope value should not change");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,25 @@
import static org.mockito.Mockito.*;

import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;

import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IFolder;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.jface.viewers.StructuredSelection;
import org.eclipse.lsp4e.LSPEclipseUtils;
import org.eclipse.lsp4j.WorkspaceFolder;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.MockedStatic;
import org.mockito.junit.jupiter.MockitoExtension;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;

import com.microsoft.copilot.eclipse.core.utils.FileUtils;

Expand All @@ -35,21 +43,40 @@ class ResourceUtilsTest {

@Mock
private IFolder mockFolder;

@Mock
private IProject mockProjectA;

@Mock
private IProject mockProjectB;

private MockedStatic<FileUtils> mockedFileUtils;
private MockedStatic<LSPEclipseUtils> mockedLspUtils;

private static final String nameProjectA= "ProjectA";
private static final String nameProjectB= "ProjectB";

@BeforeEach
void setUp() {
mockedFileUtils = mockStatic(FileUtils.class);
mockedFileUtils.when(() -> FileUtils.isExcludedFromReferencedFiles(mockValidFile)).thenReturn(false);
mockedFileUtils.when(() -> FileUtils.isExcludedFromReferencedFiles(mockInvalidFile)).thenReturn(true);

mockedLspUtils = mockStatic(LSPEclipseUtils.class);
mockedLspUtils.when(() -> LSPEclipseUtils.toWorkspaceFolder(mockProjectA))
.thenReturn(new WorkspaceFolder("file:///" + nameProjectA, nameProjectA));
mockedLspUtils.when(() -> LSPEclipseUtils.toWorkspaceFolder(mockProjectB))
.thenReturn(new WorkspaceFolder("file:///" + nameProjectB, nameProjectB));
}

@AfterEach
void tearDown() {
if (mockedFileUtils != null) {
mockedFileUtils.close();
}
if (mockedLspUtils != null) {
mockedLspUtils.close();
}
}

@Test
Expand Down Expand Up @@ -82,4 +109,52 @@ void testCollectValidResourcesWithMocks() {
assertTrue(validResources.contains(mockFolder), "Should contain folder");
assertFalse(validResources.contains(mockInvalidFile), "Should not contain excluded file");
}

private static Stream<List<IResource>> provideResourcesForNeverNullTest() {
return Stream.of(
null,
List.of(),
Arrays.asList((IResource) null),
List.of(mock(IFolder.class)),
List.of(mock(IProject.class))
);
}

@ParameterizedTest
@MethodSource("provideResourcesForNeverNullTest")
void testDeriveWorkspaceFoldersReturnsNeverNull(List<IResource> resources) {
List<WorkspaceFolder> result = ResourceUtils.deriveWorkspaceFoldersFrom(resources);

assertNotNull(result);
assertTrue(result.isEmpty());
}

@Test
void testDeriveWorkspaceFoldersWithMultipleResources() {
when(mockValidFile.getProject()).thenReturn(mockProjectA);
when(mockFolder.getProject()).thenReturn(mockProjectB);
when(mockProjectA.isAccessible()).thenReturn(true);
when(mockProjectB.isAccessible()).thenReturn(true);

List<WorkspaceFolder> result = ResourceUtils.deriveWorkspaceFoldersFrom(List.of(mockValidFile, mockFolder));

assertNotNull(result);
assertFalse(result.isEmpty());
assertEquals(2, result.size(), "Both projects from both resources should be derived as workspace folders");
}

@Test
void testDeriveWorkspaceFoldersDoesNotReturnDuplicates() {
when(mockValidFile.getProject()).thenReturn(mockProjectA);
when(mockFolder.getProject()).thenReturn(mockProjectA);
when(mockProjectA.isAccessible()).thenReturn(true);
when(mockProjectA.getName()).thenReturn(nameProjectA);

List<WorkspaceFolder> result = ResourceUtils.deriveWorkspaceFoldersFrom(List.of(mockValidFile, mockFolder));

assertNotNull(result);
assertFalse(result.isEmpty());
assertEquals(1, result.size(), "Projects in derived workspaces folders should be unique, no duplicates.");
assertEquals(mockProjectA.getName(), result.get(0).getName());
}
}
Loading