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
@@ -1,6 +1,7 @@
package com.anthropic.claudecode.eclipse.editor;

import java.net.URI;
import java.net.URISyntaxException;
import java.util.function.Function;

import org.eclipse.core.resources.IResource;
Expand Down Expand Up @@ -59,6 +60,46 @@ public static String pathOf(IEditorInput input) {
return null;
}

/**
* Whether {@code path} is not a path on the local disk but a URI of another file system
* ({@code semanticfs:/E10/...} for ABAP, a remote project's): what {@link #pathOf(IResource)}
* reports for a file that has no disk path. A Windows drive letter ({@code C:\x}) is not a
* scheme, nor is {@code file:}, which names a disk file.
*/
public static boolean isNonDiskPath(String path) {
String scheme = schemeOf(path);
return scheme != null && !scheme.equalsIgnoreCase("file");
}

/** The URI {@code path} names when {@link #isNonDiskPath} says it is one, else null. */
public static URI nonDiskUriOf(String path) {
if (!isNonDiskPath(path)) return null;
try {
return new URI(path);
} catch (URISyntaxException e) {
return null;
}
}

/** What a tool that reads and writes by disk path says when it is handed a URI of another file system. */
public static String notOnDiskMessage(String path) {
return path + " is not a file on disk: the \"" + schemeOf(path) + "\" file system keeps it, so this tool, "
+ "which reads and writes files by path, cannot work on it. Use the editor it is open in instead.";
}

/** The scheme of a URI-looking path (two or more characters, so not a drive letter), or null. */
private static String schemeOf(String path) {
if (path == null) return null;
int colon = path.indexOf(':');
if (colon < 2) return null;
if (!Character.isLetter(path.charAt(0))) return null;
for (int i = 1; i < colon; i++) {
char c = path.charAt(i);
if (!Character.isLetterOrDigit(c) && c != '+' && c != '-' && c != '.') return null;
}
return path.substring(0, colon);
}

/**
* The disk path of a workspace resource. One kept on a file system other than the
* local one (ABAP's {@code semanticfs:}, a remote project) has no disk path, so its
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import org.eclipse.ui.IEditorReference;
import org.eclipse.ui.IWorkbenchPage;

import com.anthropic.claudecode.eclipse.editor.EditorParts;
import com.anthropic.claudecode.eclipse.editor.UiHelper;
import com.anthropic.claudecode.eclipse.mcp.McpTool;
import com.anthropic.claudecode.eclipse.mcp.McpToolResult;
Expand Down Expand Up @@ -48,6 +49,7 @@ public JsonObject inputSchema() {
@Override
public McpToolResult execute(JsonObject params) {
String filePath = EditorUtils.requireString(params, "file_path", "filePath");
if (EditorParts.isNonDiskPath(filePath)) return McpToolResult.error(EditorParts.notOnDiskMessage(filePath));

DiffRegistry.DiffEntry entry = DiffRegistry.getInstance().get(filePath);
if (entry == null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@

import com.anthropic.claudecode.eclipse.Activator;
import com.anthropic.claudecode.eclipse.Constants;
import com.anthropic.claudecode.eclipse.editor.EditorParts;
import com.anthropic.claudecode.eclipse.editor.UiHelper;
import com.anthropic.claudecode.eclipse.mcp.McpTool;
import com.anthropic.claudecode.eclipse.mcp.McpToolResult;
Expand Down Expand Up @@ -99,6 +100,7 @@ public JsonObject inputSchema() {
@Override
public McpToolResult execute(JsonObject params) {
String filePath = EditorUtils.requireString(params, "old_file_path", "filePath");
if (EditorParts.isNonDiskPath(filePath)) return McpToolResult.error(EditorParts.notOnDiskMessage(filePath));
String newContent = EditorUtils.requireString(params, "new_file_contents", "newContent");
String rawTabLabel = EditorUtils.getString(params, "tab_name", "tabLabel");
String tabLabel = rawTabLabel != null ? rawTabLabel
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
package com.anthropic.claudecode.eclipse.tools;

import java.net.URI;
import java.nio.file.Path;

import org.eclipse.core.filesystem.EFS;
import org.eclipse.core.filesystem.IFileStore;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.jface.text.IDocument;
import org.eclipse.ui.IEditorPart;
import org.eclipse.ui.IWorkbenchPage;
import org.eclipse.ui.ide.IDE;
import org.eclipse.ui.texteditor.ITextEditor;

import com.anthropic.claudecode.eclipse.editor.EditorParts;
import com.anthropic.claudecode.eclipse.editor.UiHelper;
import com.anthropic.claudecode.eclipse.mcp.McpTool;
import com.anthropic.claudecode.eclipse.mcp.McpToolResult;
Expand Down Expand Up @@ -77,10 +81,20 @@ public McpToolResult execute(JsonObject params) {
return McpToolResult.error("No active workbench page");
}

IFileStore fileStore = EFS.getLocalFileSystem().getStore(Path.of(filePath).toUri());
IEditorPart editor = IDE.openEditorOnFileStore(page, fileStore);
IEditorPart editor;
if (EditorParts.isNonDiskPath(filePath)) {
// A file another file system keeps (ABAP's semanticfs:): named by the URI
// getOpenEditors reports, and opened through the workspace, not the disk.
editor = openByUri(page, filePath);
if (editor == null) return McpToolResult.error("No file found for: " + filePath);
} else {
IFileStore fileStore = EFS.getLocalFileSystem().getStore(Path.of(filePath).toUri());
editor = IDE.openEditorOnFileStore(page, fileStore);
}

if (editor instanceof ITextEditor textEditor && line > 0) {
// A multi-page editor's text is the page it is showing, not the editor itself.
ITextEditor textEditor = EditorParts.textEditorOf(editor);
if (textEditor != null && line > 0) {
IDocument doc = textEditor.getDocumentProvider().getDocument(textEditor.getEditorInput());
if (doc != null) {
int lineOffset = doc.getLineOffset(line - 1);
Expand Down Expand Up @@ -109,4 +123,18 @@ public McpToolResult execute(JsonObject params) {
}
});
}

/**
* Opens the file a non-disk URI names: the workspace's own file when it has one (what the
* file's own editor, such as ADT's, expects to be given), else whatever the file system
* behind the URI has there. Null when there is no such file.
*/
private static IEditorPart openByUri(IWorkbenchPage page, String path) throws Exception {
URI uri = EditorParts.nonDiskUriOf(path);
if (uri == null) return null;
IFile[] files = ResourcesPlugin.getWorkspace().getRoot().findFilesForLocationURI(uri);
if (files.length > 0) return IDE.openEditor(page, files[0]);
IFileStore store = EFS.getStore(uri);
return store.fetchInfo().exists() ? IDE.openEditorOnFileStore(page, store) : null;
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package com.anthropic.claudecode.eclipse.editor;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.lang.reflect.Proxy;
import java.net.URI;
Expand Down Expand Up @@ -172,4 +174,44 @@ void anyOtherInputGivesNoPath() {
assertNull(EditorParts.pathOf(fake(IEditorInput.class, Map.of())));
assertNull(EditorParts.pathOf((IEditorInput) null));
}
// ---- non-disk paths ----

@Test
void aUriOfAnotherFileSystemIsNotADiskPath() {
assertTrue(EditorParts.isNonDiskPath("semanticfs:/E10_900_user_de/.adt/programs/zdemo.prog"));
assertTrue(EditorParts.isNonDiskPath("semanticfs:/E10/%2FDMO%2FCL_FLIGHT.aclass"));
assertTrue(EditorParts.isNonDiskPath("http://example.com/a"));
}

@Test
void everyKindOfDiskPathIsADiskPath() {
for (String path : new String[] { "/home/me/work/A.java", "/home/me/a:b/c.txt", "C:\\ws\\A.java", "D:/ws/a.txt",
"\\\\server\\share\\x.txt", "file:/C:/ws/notes.txt", "FILE:///tmp/x", "relative/A.java", "", "9:x" }) {
assertFalse(EditorParts.isNonDiskPath(path), path);
}
assertFalse(EditorParts.isNonDiskPath(null));
}

@Test
void aNonDiskPathGivesItsUriAndADiskPathGivesNone() {
String path = "semanticfs:/E10_900_user_de/zdemo.prog";
assertEquals(URI.create(path), EditorParts.nonDiskUriOf(path));
assertNull(EditorParts.nonDiskUriOf("/home/me/A.java"));
assertNull(EditorParts.nonDiskUriOf("C:\\ws\\A.java"));
assertNull(EditorParts.nonDiskUriOf(null));
}

@Test
void aNonDiskPathThatIsNotAValidUriGivesNoUriButIsStillNotADiskPath() {
String path = "semanticfs:/E10/a file with spaces.prog";
assertTrue(EditorParts.isNonDiskPath(path));
assertNull(EditorParts.nonDiskUriOf(path));
}

@Test
void theMessageForANonDiskPathNamesThePathAndTheFileSystem() {
String message = EditorParts.notOnDiskMessage("semanticfs:/E10/zdemo.prog");
assertTrue(message.startsWith("semanticfs:/E10/zdemo.prog is not a file on disk"), message);
assertTrue(message.contains("\"semanticfs\" file system"), message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.anthropic.claudecode.eclipse.tools;

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

import org.junit.jupiter.api.Test;

import com.anthropic.claudecode.eclipse.mcp.McpToolResult;
import com.google.gson.JsonObject;

/**
* The diff tools read and write files by disk path, so a file another file system keeps (ABAP's
* {@code semanticfs:}, #153) is refused with a plain message, before anything touches the
* workbench: this runs with none. It used to fail on the path, or be taken for a new file.
*/
class NonDiskDiffToolsTest {

private static final String ABAP = "semanticfs:/E10_900_user_de/.adt/programs/zdemo.prog";

private static void assertRefused(McpToolResult result) {
assertTrue(result.isError(), "refused");
String text = result.toJson().toString();
assertTrue(text.contains("is not a file on disk"), text);
assertTrue(text.contains(ABAP), text);
}

@Test
void openDiffRefusesAFileThatIsNotOnDisk() {
JsonObject params = new JsonObject();
params.addProperty("old_file_path", ABAP);
params.addProperty("new_file_contents", "REPORT zdemo.");
assertRefused(new OpenDiffTool().execute(params));
}

@Test
void acceptDiffRefusesAFileThatIsNotOnDisk() {
JsonObject params = new JsonObject();
params.addProperty("file_path", ABAP);
assertRefused(new AcceptDiffTool().execute(params));
}
}
Loading