Skip to content
Merged
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,39 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

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

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

import java.nio.file.Path;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;

class FileUtilsTests {

@Test
void testGetLocalFilePath_absolutePath_returnsNormalizedPath(@TempDir Path tempDir) {
Path expected = tempDir.resolve("external-file.txt").toAbsolutePath().normalize();

assertEquals(expected, FileUtils.getLocalFilePath(expected.toString()));
}

@Test
void testGetLocalFilePath_fileUriWithFragment_ignoresFragment(@TempDir Path tempDir) {
Path expected = tempDir.resolve("external-file.txt").toAbsolutePath().normalize();

assertEquals(expected, FileUtils.getLocalFilePath(expected.toUri() + "#L10"));
}

@Test
void testGetLocalFilePath_relativePath_returnsNull() {
assertNull(FileUtils.getLocalFilePath("src/main/java/File.java"));
}

@Test
void testGetLocalFilePath_nonFileUri_returnsNull() {
assertNull(FileUtils.getLocalFilePath("https://example.com/file.java"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,53 @@ public static IFile getFileFromUri(String fileUri) {
return null;
}

/**
* Resolves an absolute local filesystem path from a path or file URI.
*
* @param filePath the path or URI to resolve
* @return the local filesystem path, or null if the input is not an absolute local path
*/
@Nullable
public static Path getLocalFilePath(String filePath) {
if (StringUtils.isBlank(filePath)) {
return null;
}
Comment thread
xinyi-gong marked this conversation as resolved.

try {
// For file URIs, '#' is always a fragment delimiter (literal '#' in filenames is encoded as %23).
if (filePath.startsWith("file:")) {
String uriWithoutFragment = stripFragment(filePath);
return Paths.get(new URI(uriWithoutFragment)).toAbsolutePath().normalize();
}

String pathWithoutFragment = stripFragment(filePath);
if (URI_SCHEME_PATTERN.matcher(pathWithoutFragment).find() && !hasDriveLetter(pathWithoutFragment)) {
Comment thread
jdneo marked this conversation as resolved.
return null;
}

// For raw paths, try the full string first since '#' is a valid filename character on Unix/Linux.
// Only fall back to stripping the fragment if the full path doesn't exist.
Path fullPath = Paths.get(filePath);
if (fullPath.isAbsolute()) {
if (Files.exists(fullPath)) {
return fullPath.toAbsolutePath().normalize();
}
// Fall back: treat '#...' as a line-number fragment
Path strippedPath = Paths.get(pathWithoutFragment);
return strippedPath.isAbsolute() ? strippedPath.toAbsolutePath().normalize() : null;
}
return null;
} catch (IllegalArgumentException | URISyntaxException e) {
CopilotCore.LOGGER.error("Invalid local file path: " + filePath, e);
return null;
}
}

private static String stripFragment(String pathOrUri) {
int fragmentIndex = pathOrUri.indexOf('#');
Comment thread
xinyi-gong marked this conversation as resolved.
return fragmentIndex > 0 ? pathOrUri.substring(0, fragmentIndex) : pathOrUri;
}

/**
* Normalizes a file path or URI string to a proper file URI string. Handles Windows absolute paths, POSIX absolute
* paths, and existing URI strings. Line number fragments (e.g., #L123) are preserved.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -192,3 +192,30 @@ Not exercised:
#### Key Screenshots
- [ ] **Before created-file Undo** -- The summary bar lists `created-local-file.txt`.
- [ ] **After created-file Undo** -- The summary bar is clear and the file is absent from disk.

---

## 3. Navigate to local files from tool links

### TC-006: Tool result links open local files outside the workspace

**Type:** `Happy Path`
**Priority:** `P0`

#### Preconditions
- The Eclipse workbench is open.
- Copilot Chat is open in Agent mode.
- The local test directory outside the workspace exists and contains `existing-local-file.txt`.

#### Steps
1. Send a prompt that causes Agent mode to reference or edit `existing-local-file.txt` by absolute path.
2. When the tool call appears in the Chat view, click the file path link for `existing-local-file.txt`.
3. Verify Eclipse opens `existing-local-file.txt` in an editor.

#### Expected Result
- File links for paths outside the Eclipse workspace open the local file in an Eclipse editor.
- No error dialog is shown and the Eclipse error log has no local file navigation exception.

#### Key Screenshots
- [ ] **Local file tool link** -- The tool result shows a clickable absolute path outside the workspace.
- [ ] **External local file editor** -- The external local file opens in an Eclipse editor.
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@

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

import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;

import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.Path;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.hyperlink.IHyperlink;
import org.eclipse.jface.text.hyperlink.URLHyperlink;
Expand Down Expand Up @@ -59,14 +62,27 @@ public void open() {
String urlString = getURLString();
if (urlString.startsWith(LSPEclipseUtils.FILE_URI)) {
IResource targetResource = LSPEclipseUtils.findResourceFor(urlString);
if (targetResource != null && targetResource.getType() == IResource.FILE) {
Location location = new Location();
location.setUri(urlString);
LSPEclipseUtils.openInEditor(location);
if (targetResource != null) {
if (targetResource.getType() == IResource.FILE) {
Location location = new Location();
location.setUri(urlString);
LSPEclipseUtils.openInEditor(location);
return;
}
if (targetResource.getType() == IResource.FOLDER
|| targetResource.getType() == IResource.PROJECT) {
UiUtils.revealInExplorer(targetResource);
return;
}
}
Path localPath = FileUtils.getLocalFilePath(urlString);
if (localPath != null && Files.isRegularFile(localPath, LinkOption.NOFOLLOW_LINKS)) {
UiUtils.openLocalFileInEditor(localPath);
return;
Comment thread
xinyi-gong marked this conversation as resolved.
}
} else {
IFile file = ResourcesPlugin.getWorkspace().getRoot().getFile(new Path(urlString));
IFile file = ResourcesPlugin.getWorkspace().getRoot()
.getFile(new org.eclipse.core.runtime.Path(urlString));
Comment thread
xinyi-gong marked this conversation as resolved.

if (file.exists()) {
var workbenchWindow = PlatformUI.getWorkbench().getActiveWorkbenchWindow();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ private LanguageModelToolResult createFile(String filePath, String content) {
return createWorkspaceFile(file, filePath, content);
}

Path localPath = getLocalFilePath(filePath);
Path localPath = FileUtils.getLocalFilePath(filePath);
if (localPath != null) {
return createLocalFile(localPath, content);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ private LanguageModelToolResult[] editFile(String filePath, String code) {
return editWorkspaceFile(file, code);
}

Path localPath = getLocalFilePath(filePath);
Path localPath = FileUtils.getLocalFilePath(filePath);
if (localPath != null && Files.isRegularFile(localPath, LinkOption.NOFOLLOW_LINKS)) {
return editLocalFile(localPath, code);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,9 @@
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.InvocationTargetException;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
Expand Down Expand Up @@ -297,24 +294,7 @@ protected Path normalizeLocalPath(Path file) {
return file.toAbsolutePath().normalize();
}

/**
* Resolves an absolute local filesystem path from a path or file URI.
*
* @param filePath the path or URI to resolve
* @return the local filesystem path, or null if the input is not an absolute local path
*/
protected Path getLocalFilePath(String filePath) {
try {
if (filePath.startsWith("file:")) {
return Paths.get(new URI(filePath));
}
Path path = Paths.get(filePath);
return path.isAbsolute() ? path : null;
} catch (IllegalArgumentException | URISyntaxException e) {
CopilotCore.LOGGER.error("Invalid local file path: " + filePath, e);
return null;
}
}


private CompareEditorInput createWorkspaceCompareEditorInput(String comparedContent, IFile file) {
ChangedFile changedFile = ChangedFile.workspace(file);
Expand Down
Loading