From 1d7aac54c2b33ef83a320ddef7a3a99af4a07fa9 Mon Sep 17 00:00:00 2001 From: xinyi-gong Date: Mon, 1 Jun 2026 16:58:33 +0800 Subject: [PATCH 1/3] Fix file and directory link navigation in chat tool results --- .../copilot/eclipse/core/utils/FileUtils.java | 34 +++++++++++++++++++ .../local-file-edit-and-create-tools.md | 27 +++++++++++++++ .../chat/FileAnnotationHyperlinkDetector.java | 27 +++++++++++---- .../eclipse/ui/chat/tools/CreateFileTool.java | 2 +- .../eclipse/ui/chat/tools/EditFileTool.java | 2 +- .../eclipse/ui/chat/tools/FileToolBase.java | 22 +----------- 6 files changed, 85 insertions(+), 29 deletions(-) diff --git a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/utils/FileUtils.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/utils/FileUtils.java index 3dd51c39..a12f3f46 100644 --- a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/utils/FileUtils.java +++ b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/utils/FileUtils.java @@ -181,6 +181,40 @@ 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; + } + + try { + String pathWithoutFragment = stripFragment(filePath); + if (pathWithoutFragment.startsWith("file:")) { + return Paths.get(new URI(pathWithoutFragment)).toAbsolutePath().normalize(); + } + if (URI_SCHEME_PATTERN.matcher(pathWithoutFragment).find() && !hasDriveLetter(pathWithoutFragment)) { + return null; + } + + Path path = Paths.get(pathWithoutFragment); + return path.isAbsolute() ? path.toAbsolutePath().normalize() : 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('#'); + 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. diff --git a/com.microsoft.copilot.eclipse.swtbot.test/test-plans/file-system/local-file-edit-and-create-tools.md b/com.microsoft.copilot.eclipse.swtbot.test/test-plans/file-system/local-file-edit-and-create-tools.md index ae46796c..7f9a18cf 100644 --- a/com.microsoft.copilot.eclipse.swtbot.test/test-plans/file-system/local-file-edit-and-create-tools.md +++ b/com.microsoft.copilot.eclipse.swtbot.test/test-plans/file-system/local-file-edit-and-create-tools.md @@ -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. diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/FileAnnotationHyperlinkDetector.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/FileAnnotationHyperlinkDetector.java index eef5dc17..aa4b6472 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/FileAnnotationHyperlinkDetector.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/FileAnnotationHyperlinkDetector.java @@ -3,10 +3,12 @@ package com.microsoft.copilot.eclipse.ui.chat; +import java.nio.file.Files; +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; @@ -59,14 +61,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)) { + UiUtils.openLocalFileInEditor(localPath); return; } } else { - IFile file = ResourcesPlugin.getWorkspace().getRoot().getFile(new Path(urlString)); + IFile file = ResourcesPlugin.getWorkspace().getRoot() + .getFile(new org.eclipse.core.runtime.Path(urlString)); if (file.exists()) { var workbenchWindow = PlatformUI.getWorkbench().getActiveWorkbenchWindow(); diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/tools/CreateFileTool.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/tools/CreateFileTool.java index c7795aeb..ca9485dd 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/tools/CreateFileTool.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/tools/CreateFileTool.java @@ -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); } diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/tools/EditFileTool.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/tools/EditFileTool.java index 6a20fcd6..1260b0a1 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/tools/EditFileTool.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/tools/EditFileTool.java @@ -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); } diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/tools/FileToolBase.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/tools/FileToolBase.java index 3ad526bf..8d545fd7 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/tools/FileToolBase.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/tools/FileToolBase.java @@ -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; @@ -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); From b151f6aa9e74a5201d8d59689e231ba31f651359 Mon Sep 17 00:00:00 2001 From: xinyi-gong Date: Mon, 1 Jun 2026 17:24:44 +0800 Subject: [PATCH 2/3] resolve comments --- .../eclipse/core/utils/FileUtilsTests.java | 39 +++++++++++++++++++ .../chat/FileAnnotationHyperlinkDetector.java | 3 +- 2 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 com.microsoft.copilot.eclipse.core.test/src/com/microsoft/copilot/eclipse/core/utils/FileUtilsTests.java diff --git a/com.microsoft.copilot.eclipse.core.test/src/com/microsoft/copilot/eclipse/core/utils/FileUtilsTests.java b/com.microsoft.copilot.eclipse.core.test/src/com/microsoft/copilot/eclipse/core/utils/FileUtilsTests.java new file mode 100644 index 00000000..cbbf6d55 --- /dev/null +++ b/com.microsoft.copilot.eclipse.core.test/src/com/microsoft/copilot/eclipse/core/utils/FileUtilsTests.java @@ -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")); + } +} diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/FileAnnotationHyperlinkDetector.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/FileAnnotationHyperlinkDetector.java index aa4b6472..2a4b377b 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/FileAnnotationHyperlinkDetector.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/FileAnnotationHyperlinkDetector.java @@ -4,6 +4,7 @@ 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; @@ -75,7 +76,7 @@ public void open() { } } Path localPath = FileUtils.getLocalFilePath(urlString); - if (localPath != null && Files.isRegularFile(localPath)) { + if (localPath != null && Files.isRegularFile(localPath, LinkOption.NOFOLLOW_LINKS)) { UiUtils.openLocalFileInEditor(localPath); return; } From c3f5e7dff67ad0b6428b162a5686625d7f4722bc Mon Sep 17 00:00:00 2001 From: xinyi-gong Date: Tue, 2 Jun 2026 10:05:55 +0800 Subject: [PATCH 3/3] resolve comments --- .../copilot/eclipse/core/utils/FileUtils.java | 23 +++++++++++++++---- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/utils/FileUtils.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/utils/FileUtils.java index a12f3f46..06bbb611 100644 --- a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/utils/FileUtils.java +++ b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/utils/FileUtils.java @@ -194,16 +194,29 @@ public static Path getLocalFilePath(String filePath) { } try { - String pathWithoutFragment = stripFragment(filePath); - if (pathWithoutFragment.startsWith("file:")) { - return Paths.get(new URI(pathWithoutFragment)).toAbsolutePath().normalize(); + // 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)) { return null; } - Path path = Paths.get(pathWithoutFragment); - return path.isAbsolute() ? path.toAbsolutePath().normalize() : 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;